{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "typing-text",
  "type": "registry:ui",
  "title": "Typing Text",
  "description": "A text component that dynamically simulates a typing animation, progressively revealing characters as if typed in real-time.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/text/typing/index.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\nimport { motion, useInView, type UseInViewOptions } from 'motion/react';\n\nimport { cn } from '@/lib/utils';\n\nfunction CursorBlinker({ className }: { className?: string }) {\n  return (\n    <motion.span\n      data-slot=\"cursor-blinker\"\n      variants={{\n        blinking: {\n          opacity: [0, 0, 1, 1],\n          transition: {\n            duration: 1,\n            repeat: Infinity,\n            repeatDelay: 0,\n            ease: 'linear',\n            times: [0, 0.5, 0.5, 1],\n          },\n        },\n      }}\n      animate=\"blinking\"\n      className={cn(\n        'inline-block h-5 w-[1px] translate-y-1 bg-black dark:bg-white',\n        className,\n      )}\n    />\n  );\n}\n\ntype TypingTextProps = Omit<React.ComponentProps<'span'>, 'children'> & {\n  duration?: number;\n  delay?: number;\n  inView?: boolean;\n  inViewMargin?: UseInViewOptions['margin'];\n  inViewOnce?: boolean;\n  cursor?: boolean;\n  loop?: boolean;\n  holdDelay?: number;\n  text: string | string[];\n  cursorClassName?: string;\n  animateOnChange?: boolean;\n};\n\nfunction TypingText({\n  ref,\n  duration = 100,\n  delay = 0,\n  inView = false,\n  inViewMargin = '0px',\n  inViewOnce = true,\n  cursor = false,\n  loop = false,\n  holdDelay = 1000,\n  text,\n  cursorClassName,\n  animateOnChange = true,\n  ...props\n}: TypingTextProps) {\n  const localRef = React.useRef<HTMLSpanElement>(null);\n  React.useImperativeHandle(ref, () => localRef.current as HTMLSpanElement);\n\n  const inViewResult = useInView(localRef, {\n    once: inViewOnce,\n    margin: inViewMargin,\n  });\n  const isInView = !inView || inViewResult;\n\n  const [started, setStarted] = React.useState(false);\n  const [displayedText, setDisplayedText] = React.useState<string>('');\n\n  React.useEffect(() => {\n    // Reset animation when text changes (if animateOnChange is true)\n    if (animateOnChange) {\n      setStarted(false);\n      setDisplayedText('');\n    }\n\n    if (isInView) {\n      const timeoutId = setTimeout(() => {\n        setStarted(true);\n      }, delay);\n      return () => clearTimeout(timeoutId);\n    } else {\n      const timeoutId = setTimeout(() => {\n        setStarted(true);\n      }, delay);\n      return () => clearTimeout(timeoutId);\n    }\n  }, [isInView, delay, ...(animateOnChange ? [text] : [])]);\n\n  React.useEffect(() => {\n    if (!started) return;\n    const timeoutIds: Array<ReturnType<typeof setTimeout>> = [];\n    const texts: string[] = typeof text === 'string' ? [text] : text;\n\n    const typeText = (str: string, onComplete: () => void) => {\n      let currentIndex = 0;\n      const type = () => {\n        if (currentIndex <= str.length) {\n          setDisplayedText(str.substring(0, currentIndex));\n          currentIndex++;\n          const id = setTimeout(type, duration);\n          timeoutIds.push(id);\n        } else {\n          onComplete();\n        }\n      };\n      type();\n    };\n\n    const eraseText = (str: string, onComplete: () => void) => {\n      let currentIndex = str.length;\n      const erase = () => {\n        if (currentIndex >= 0) {\n          setDisplayedText(str.substring(0, currentIndex));\n          currentIndex--;\n          const id = setTimeout(erase, duration);\n          timeoutIds.push(id);\n        } else {\n          onComplete();\n        }\n      };\n      erase();\n    };\n\n    const animateTexts = (index: number) => {\n      typeText(texts[index] ?? '', () => {\n        const isLast = index === texts.length - 1;\n        if (isLast && !loop) {\n          return;\n        }\n        const id = setTimeout(() => {\n          eraseText(texts[index] ?? '', () => {\n            const nextIndex = isLast ? 0 : index + 1;\n            animateTexts(nextIndex);\n          });\n        }, holdDelay);\n        timeoutIds.push(id);\n      });\n    };\n\n    animateTexts(0);\n\n    return () => {\n      timeoutIds.forEach(clearTimeout);\n    };\n  }, [text, duration, started, loop, holdDelay]);\n\n  return (\n    <span ref={localRef} data-slot=\"typing-text\" {...props}>\n      <motion.span>{displayedText}</motion.span>\n      {cursor && <CursorBlinker className={cursorClassName} />}\n    </span>\n  );\n}\n\nexport { TypingText, type TypingTextProps };\n",
      "type": "registry:ui",
      "target": "components/animate-ui/text/typing.tsx"
    }
  ]
}