{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "rotating-text",
  "title": "Rotating Text",
  "description": "Cycling words that roll letter by letter, with width easing.",
  "dependencies": [],
  "registryDependencies": [
    "utils",
    "https://ui.cuelark.com/r/theme.json",
    "https://ui.cuelark.com/r/motion.json"
  ],
  "files": [
    {
      "path": "src/registry/cuelark/ui/rotating-text.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { gsap, motionScale } from \"@/lib/motion\";\nimport { cn } from \"@/lib/utils\";\n\ntype RotatingTextProps = Omit<React.ComponentProps<\"span\">, \"children\"> & {\n  words: string[];\n  /** Milliseconds each word stays. */\n  interval?: number;\n};\n\n/**\n * Cycles through words in place: the old word rolls up and away letter by letter, the new one rolls in, and\n * the box eases to the new width so surrounding text never jumps. Stops on hover; static with motion off.\n */\nfunction RotatingText({ words, interval = 2400, className, ...props }: RotatingTextProps) {\n  const [index, setIndex] = React.useState(0);\n  const [paused, setPaused] = React.useState(false);\n  const boxRef = React.useRef<HTMLSpanElement>(null);\n  const previous = React.useRef<string | null>(null);\n\n  React.useEffect(() => {\n    const box = boxRef.current;\n    if (paused || words.length < 2 || !box || !motionScale(box)) return;\n    const id = setInterval(() => setIndex((i) => (i + 1) % words.length), interval);\n    return () => clearInterval(id);\n  }, [paused, interval, words.length]);\n\n  React.useLayoutEffect(() => {\n    const box = boxRef.current;\n    if (!box) return;\n    const current = box.querySelector<HTMLElement>('[data-slot=\"rotating-word\"]');\n    const old = box.querySelector<HTMLElement>('[data-slot=\"rotating-old\"]');\n    if (!current || previous.current === null) {\n      previous.current = words[index] ?? \"\";\n      return;\n    }\n    const scale = motionScale(box);\n    const fromWidth = old?.offsetWidth ?? current.offsetWidth;\n    gsap.fromTo(box, { width: fromWidth }, { width: current.offsetWidth, duration: 0.5, ease: \"cue\", clearProps: \"width\" });\n    gsap.fromTo(current.children, { yPercent: 100, opacity: 0 }, { yPercent: 0, opacity: 1, duration: 0.5, ease: \"cue\", stagger: 0.025 * scale });\n    if (old) gsap.to(old.children, { yPercent: -100, opacity: 0, duration: 0.35, ease: \"cue\", stagger: 0.02 * scale });\n    previous.current = words[index] ?? \"\";\n  }, [index, words]);\n\n  const letters = (word: string) => [...word].map((c, i) => <span key={i} className=\"inline-block whitespace-pre\">{c}</span>);\n  const word = words[index] ?? \"\";\n  const old = previous.current !== null && previous.current !== word ? previous.current : null;\n\n  return (\n    <span\n      ref={boxRef}\n      data-slot=\"rotating-text\"\n      className={cn(\"relative inline-flex overflow-hidden align-bottom\", className)}\n      onPointerEnter={() => setPaused(true)}\n      onPointerLeave={() => setPaused(false)}\n      {...props}\n    >\n      {old && (\n        <span key={`old-${index}`} data-slot=\"rotating-old\" aria-hidden=\"true\" className=\"absolute inset-y-0 left-0 inline-flex\">\n          {letters(old)}\n        </span>\n      )}\n      <span key={index} data-slot=\"rotating-word\" className=\"inline-flex\" aria-label={word}>\n        {letters(word)}\n      </span>\n    </span>\n  );\n}\n\nexport { RotatingText };\nexport type { RotatingTextProps };\n",
      "type": "registry:ui"
    }
  ],
  "docs": "Docs and examples: https://ui.cuelark.com/components/rotating-text",
  "categories": [
    "motion"
  ],
  "type": "registry:ui"
}