{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "typing-animation",
  "type": "registry:ui",
  "title": "Typing Animation",
  "description": "Characters appearing in typed animation",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/magicui/typing-animation.tsx",
      "content": "\"use client\"\n\nimport {\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n  type ComponentType,\n  type RefAttributes,\n  type RefObject,\n} from \"react\"\nimport {\n  motion,\n  useInView,\n  type DOMMotionComponents,\n  type HTMLMotionProps,\n  type MotionProps,\n} from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst motionElements = {\n  article: motion.article,\n  div: motion.div,\n  h1: motion.h1,\n  h2: motion.h2,\n  h3: motion.h3,\n  h4: motion.h4,\n  h5: motion.h5,\n  h6: motion.h6,\n  li: motion.li,\n  p: motion.p,\n  section: motion.section,\n  span: motion.span,\n} as const\n\ntype MotionElementType = Extract<\n  keyof DOMMotionComponents,\n  keyof typeof motionElements\n>\ntype TypingAnimationMotionComponent = ComponentType<\n  Omit<HTMLMotionProps<\"span\">, \"ref\"> & RefAttributes<HTMLElement>\n>\n\ninterface TypingAnimationProps extends Omit<MotionProps, \"children\"> {\n  children?: string\n  words?: string[]\n  className?: string\n  duration?: number\n  typeSpeed?: number\n  deleteSpeed?: number\n  delay?: number\n  pauseDelay?: number\n  loop?: boolean\n  as?: MotionElementType\n  startOnView?: boolean\n  showCursor?: boolean\n  blinkCursor?: boolean\n  cursorStyle?: \"line\" | \"block\" | \"underscore\"\n}\n\nexport function TypingAnimation({\n  children,\n  words,\n  className,\n  duration = 100,\n  typeSpeed,\n  deleteSpeed,\n  delay = 0,\n  pauseDelay = 1000,\n  loop = false,\n  as: Component = \"span\",\n  startOnView = true,\n  showCursor = true,\n  blinkCursor = true,\n  cursorStyle = \"line\",\n  ...props\n}: TypingAnimationProps) {\n  const MotionComponent = motionElements[\n    Component\n  ] as TypingAnimationMotionComponent\n\n  const [displayedText, setDisplayedText] = useState<string>(\"\")\n  const [currentWordIndex, setCurrentWordIndex] = useState(0)\n  const [currentCharIndex, setCurrentCharIndex] = useState(0)\n  const [phase, setPhase] = useState<\"typing\" | \"pause\" | \"deleting\">(\"typing\")\n  const elementRef = useRef<HTMLElement | null>(null)\n  const isInView = useInView(elementRef as RefObject<Element>, {\n    amount: 0.3,\n    once: true,\n  })\n\n  const wordsToAnimate = useMemo(\n    () => words ?? (children ? [children] : []),\n    [words, children]\n  )\n  const hasMultipleWords = wordsToAnimate.length > 1\n\n  const typingSpeed = typeSpeed ?? duration\n  const deletingSpeed = deleteSpeed ?? typingSpeed / 2\n\n  const shouldStart = startOnView ? isInView : true\n  const animationSourceKey = useMemo(\n    () => (words ? words.join(\"\\u0000\") : (children ?? \"\")),\n    [words, children]\n  )\n\n  useEffect(() => {\n    setDisplayedText(\"\")\n    setCurrentWordIndex(0)\n    setCurrentCharIndex(0)\n    setPhase(\"typing\")\n  }, [animationSourceKey])\n\n  useEffect(() => {\n    let timeout: ReturnType<typeof setTimeout> | null = null\n\n    if (shouldStart && wordsToAnimate.length > 0) {\n      const timeoutDelay =\n        delay > 0 && displayedText === \"\"\n          ? delay\n          : phase === \"typing\"\n            ? typingSpeed\n            : phase === \"deleting\"\n              ? deletingSpeed\n              : pauseDelay\n\n      timeout = setTimeout(() => {\n        const currentWord = wordsToAnimate[currentWordIndex] || \"\"\n        const graphemes = Array.from(currentWord)\n\n        switch (phase) {\n          case \"typing\":\n            if (currentCharIndex < graphemes.length) {\n              setDisplayedText(\n                graphemes.slice(0, currentCharIndex + 1).join(\"\")\n              )\n              setCurrentCharIndex(currentCharIndex + 1)\n            } else {\n              if (hasMultipleWords || loop) {\n                const isLastWord =\n                  currentWordIndex === wordsToAnimate.length - 1\n                if (!isLastWord || loop) {\n                  setPhase(\"pause\")\n                }\n              }\n            }\n            break\n\n          case \"pause\":\n            setPhase(\"deleting\")\n            break\n\n          case \"deleting\":\n            if (currentCharIndex > 0) {\n              setDisplayedText(\n                graphemes.slice(0, currentCharIndex - 1).join(\"\")\n              )\n              setCurrentCharIndex(currentCharIndex - 1)\n            } else {\n              const nextIndex = (currentWordIndex + 1) % wordsToAnimate.length\n              setCurrentWordIndex(nextIndex)\n              setPhase(\"typing\")\n            }\n            break\n        }\n      }, timeoutDelay)\n    }\n\n    return () => {\n      if (timeout !== null) {\n        clearTimeout(timeout)\n      }\n    }\n  }, [\n    shouldStart,\n    phase,\n    currentCharIndex,\n    currentWordIndex,\n    displayedText,\n    wordsToAnimate,\n    hasMultipleWords,\n    loop,\n    typingSpeed,\n    deletingSpeed,\n    pauseDelay,\n    delay,\n  ])\n\n  const currentWordGraphemes = Array.from(\n    wordsToAnimate[currentWordIndex] || \"\"\n  )\n  const isComplete =\n    !loop &&\n    currentWordIndex === wordsToAnimate.length - 1 &&\n    currentCharIndex >= currentWordGraphemes.length &&\n    phase !== \"deleting\"\n\n  const shouldShowCursor =\n    showCursor &&\n    !isComplete &&\n    (hasMultipleWords || loop || currentCharIndex < currentWordGraphemes.length)\n\n  const getCursorChar = () => {\n    switch (cursorStyle) {\n      case \"block\":\n        return \"▌\"\n      case \"underscore\":\n        return \"_\"\n      case \"line\":\n      default:\n        return \"|\"\n    }\n  }\n\n  return (\n    <MotionComponent\n      ref={elementRef}\n      className={cn(\n        \"leading-20 tracking-[-0.02em]\",\n        Component === \"span\" && \"inline-block\",\n        className\n      )}\n      {...props}\n    >\n      {displayedText}\n      {shouldShowCursor && (\n        <span\n          className={cn(\"inline-block\", blinkCursor && \"animate-blink-cursor\")}\n        >\n          {getCursorChar()}\n        </span>\n      )}\n    </MotionComponent>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "cssVars": {
    "theme": {
      "animate-blink-cursor": "blink-cursor 1.2s step-end infinite"
    }
  },
  "css": {
    "@keyframes blink-cursor": {
      "0%, 49%": {
        "opacity": "1"
      },
      "50%, 100%": {
        "opacity": "0"
      }
    }
  }
}