{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "text-loop",
  "type": "registry:ui",
  "registryDependencies": [],
  "dependencies": ["motion"],
  "devDependencies": [],
  "files": [
    {
      "path": "text-loop.tsx",
      "content": "\"use client\";\n\nimport type { Transition, Variants } from \"motion/react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport {\n  Children,\n  isValidElement,\n  useCallback,\n  useEffect,\n  useMemo,\n  useState,\n} from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst useLoop = (delay = 1000) => {\n  const [key, setKey] = useState(0);\n\n  const incrementKey = useCallback(() => {\n    setKey((prev) => prev + 1);\n  }, []);\n\n  useEffect(() => {\n    const interval = setInterval(incrementKey, delay);\n    return () => clearInterval(interval);\n  }, [delay, incrementKey]);\n\n  return { key };\n};\n\nfunction nodeText(node: React.ReactNode): string {\n  if (node == null || typeof node === \"boolean\") {\n    return \"\";\n  }\n\n  if (typeof node === \"string\" || typeof node === \"number\") {\n    return String(node);\n  }\n\n  if (Array.isArray(node)) {\n    return node.map(nodeText).join(\"\");\n  }\n\n  if (isValidElement<{ children?: React.ReactNode }>(node)) {\n    return nodeText(node.props.children);\n  }\n\n  return \"\";\n}\n\nexport type TextLoopProps = {\n  children: React.ReactNode[];\n  className?: string;\n  interval?: number;\n  transition?: Transition;\n  variants?: Variants;\n  onIndexChange?: (index: number) => void;\n};\n\nfunction TextLoop({\n  children,\n  className,\n  interval = 1,\n  transition = { duration: 0.3 },\n  variants,\n  onIndexChange,\n}: TextLoopProps) {\n  const items = Children.toArray(children);\n  const { key } = useLoop(interval * 1000);\n  const currentIndex = useMemo(\n    () => (items.length > 0 ? key % items.length : 0),\n    [items.length, key]\n  );\n  const longestItem = useMemo(\n    () =>\n      items.reduce((longest, item) =>\n        nodeText(item).length > nodeText(longest).length ? item : longest\n      ),\n    [items]\n  );\n\n  useEffect(() => {\n    onIndexChange?.(currentIndex);\n  }, [currentIndex, onIndexChange]);\n\n  const motionVariants: Variants = {\n    initial: { opacity: 0, y: \"100%\" },\n    animate: { opacity: 1, y: 0 },\n    exit: { opacity: 0, y: \"-100%\" },\n  };\n\n  if (items.length === 0) {\n    return null;\n  }\n\n  return (\n    <span\n      className={cn(\n        \"relative inline-grid align-baseline leading-none\",\n        className\n      )}\n      style={{ verticalAlign: \"baseline\" }}\n    >\n      <span\n        aria-hidden\n        className=\"invisible col-start-1 row-start-1 whitespace-nowrap\"\n      >\n        {longestItem}\n      </span>\n      <span className=\"relative col-start-1 row-start-1 overflow-hidden\">\n        <AnimatePresence initial={false} mode=\"popLayout\">\n          <motion.span\n            animate=\"animate\"\n            className=\"block whitespace-nowrap\"\n            exit=\"exit\"\n            initial=\"initial\"\n            key={key}\n            transition={transition}\n            variants={variants ?? motionVariants}\n          >\n            {items[currentIndex]}\n          </motion.span>\n        </AnimatePresence>\n      </span>\n    </span>\n  );\n}\n\nexport { TextLoop, useLoop };\n",
      "type": "registry:ui"
    }
  ],
  "title": "Text Loop",
  "description": "Cycling text with vertical slide transitions—pass your own items and interval for rotating headlines or status copy."
}
