{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dancing-letters",
  "title": "Dancing Letters",
  "description": "Physics-based interactive text animations with multiple animation styles.",
  "dependencies": [
    "motion",
    "clsx",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "registry/chamaac/dancing-letters/dancing-letters.tsx",
      "content": "\"use client\";\n\nimport { LazyMotion, domAnimation, m } from \"motion/react\";\nimport { useState, useCallback, useEffect } from \"react\";\nimport { Outfit } from \"next/font/google\";\nimport { cn } from \"@/lib/utils\";\n\nexport const dancingFont = Outfit({\n  subsets: [\"latin\"],\n  variable: \"--font-dancing\",\n});\n\ninterface DancingLettersProps {\n  text?: string;\n  className?: string;\n  letterClassName?: string;\n  autoPlay?: boolean;\n  autoPlayInterval?: number;\n}\n\n// Sleek, physics-based animations\nconst letterAnimations = [\n  // 1. Rubber Band (Snap)\n  {\n    active: {\n      scaleX: [1, 1.25, 0.75, 1.15, 0.95, 1.05, 1],\n      scaleY: [1, 0.75, 1.25, 0.85, 1.05, 0.95, 1],\n    },\n    transition: { duration: 0.8, ease: \"easeInOut\" },\n    transformOrigin: \"center center\",\n  },\n  // 2. The Hinge (Falling effect)\n  {\n    active: {\n      rotate: [0, 80, 60, 80, 60, 0],\n      y: [0, 10, -5, 5, -2, 0],\n      originX: 0,\n      originY: 1,\n    },\n    transition: { duration: 1.2, ease: [0.175, 0.885, 0.32, 1.275] },\n    transformOrigin: \"bottom left\",\n  },\n  // 3. Squash and Jump\n  {\n    active: {\n      scaleY: [1, 0.6, 1.2, 1],\n      y: [0, 20, -40, 0],\n    },\n    transition: { duration: 0.6, ease: \"easeOut\" },\n    transformOrigin: \"bottom center\",\n  },\n  // 4. Falling (Requests)\n  {\n    active: {\n      rotateX: [0, 240, 150, 200, 175, 180, 180, 0],\n      scale: [1, 1.1, 1],\n    },\n    transition: {\n      duration: 2,\n      ease: \"easeOut\",\n      times: [0, 0.12, 0.24, 0.36, 0.48, 0.6, 0.85, 1],\n    },\n    transformOrigin: \"50% 80%\",\n  },\n  // 5. Elastic Slide\n  {\n    active: {\n      x: [0, -20, 15, -10, 5, 0],\n    },\n    transition: { duration: 0.8, ease: \"easeInOut\" },\n    transformOrigin: \"center center\",\n  },\n  // 6. Impact Shake\n  {\n    active: {\n      x: [0, -5, 5, -5, 5, -2, 2, 0],\n      y: [0, -2, 2, -1, 1, 0],\n      rotate: [0, -1, 1, -0.5, 0.5, 0],\n    },\n    transition: { duration: 0.5, ease: \"linear\" },\n    transformOrigin: \"center center\",\n  },\n  // 7. Pop (Scale)\n  {\n    active: {\n      scale: [1, 1.4, 1],\n    },\n    transition: { duration: 0.5, ease: \"easeInOut\" },\n    transformOrigin: \"center center\",\n  },\n  // 8. Levitate\n  {\n    active: {\n      y: [0, -30, 0],\n      scale: [1, 1.1, 1],\n      textShadow: [\n        \"0px 0px 0px rgba(0,0,0,0)\",\n        \"0px 20px 20px rgba(0,0,0,0.2)\",\n        \"0px 0px 0px rgba(0,0,0,0)\",\n      ],\n    },\n    transition: { duration: 1.2, ease: \"easeInOut\" },\n    transformOrigin: \"center center\",\n  },\n];\n\nconst DancingLetters = ({\n  text = \"ANIMATE\",\n  className = \"\",\n  letterClassName = \"\",\n}: DancingLettersProps) => {\n  const [activeIndices, setActiveIndices] = useState<Set<number>>(new Set());\n  const letters = text.split(\"\");\n  const [isLoaded, setIsLoaded] = useState(false);\n\n  useEffect(() => {\n    const timer = setTimeout(() => setIsLoaded(true), 1000);\n    return () => clearTimeout(timer);\n  }, []);\n\n  const handleClick = useCallback((index: number) => {\n    setActiveIndices((prev) => {\n      const next = new Set(prev);\n      if (next.has(index)) {\n        next.delete(index);\n      }\n      // Small timeout to allow state clear if double clicking rapidly\n      setTimeout(() => {\n        setActiveIndices((prev) => {\n          const next = new Set(prev);\n          next.add(index);\n          return next;\n        });\n      }, 10);\n      return next;\n    });\n  }, []);\n\n  const handleAnimationComplete = useCallback((index: number) => {\n    setActiveIndices((prev) => {\n      if (!prev.has(index)) return prev;\n      const next = new Set(prev);\n      next.delete(index);\n      return next;\n    });\n  }, []);\n\n  return (\n    <LazyMotion features={domAnimation}>\n      <m.div\n        className={cn(\n          \"flex items-center justify-center select-none\",\n          dancingFont.variable,\n          className\n        )}\n        style={{ perspective: \"1000px\" }}\n        initial=\"hidden\"\n        animate=\"visible\"\n        variants={{\n          hidden: { opacity: 0, y: 20 },\n          visible: {\n            opacity: 1,\n            y: 0,\n            transition: {\n              staggerChildren: 0.05,\n            },\n          },\n        }}\n      >\n        {letters.map((letter, id) => {\n          const animIndex = id % letterAnimations.length;\n          const anim = letterAnimations[animIndex];\n          const isActive = activeIndices.has(id);\n\n          return (\n            <m.span\n              key={`${letter}-${id}`}\n              variants={{\n                hidden: { opacity: 0, y: 20, scale: 0.8 },\n                visible: {\n                  opacity: 1,\n                  scale: 1,\n                  x: 0,\n                  y: 0,\n                  rotate: 0,\n                  rotateX: 0,\n                  rotateY: 0,\n                  scaleX: 1,\n                  scaleY: 1,\n                  textShadow: \"0px 0px 0px rgba(0,0,0,0)\",\n                  transition: { type: \"spring\", stiffness: 300, damping: 20 },\n                },\n                active: {\n                  ...anim.active,\n                  opacity: 1,\n                  // @ts-expect-error transition type mismatch\n                  transition: anim.transition,\n                },\n              }}\n              animate={isActive ? \"active\" : isLoaded ? \"visible\" : undefined}\n              onHoverStart={() => {\n                if (!isActive) handleClick(id);\n              }}\n              onClick={() => handleClick(id)}\n              onAnimationComplete={(definition) => {\n                if (definition === \"active\") handleAnimationComplete(id);\n              }}\n              className={cn(\n                \"relative inline-block text-5xl md:text-7xl lg:text-8xl font-bold text-neutral-900 dark:text-neutral-100 cursor-pointer\",\n                letterClassName,\n                isActive ? \"z-10\" : \"z-0\"\n              )}\n              style={{\n                transformOrigin: anim.transformOrigin,\n                transformStyle: \"preserve-3d\",\n              }}\n            >\n              {letter}\n            </m.span>\n          );\n        })}\n      </m.div>\n    </LazyMotion>\n  );\n};\n\nexport default DancingLetters;\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}