{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "typewriter",
  "title": "Typewriter",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-react/components/typewriter/Typewriter.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { cn } from '@/lib/utils'\n\nexport interface TypewriterProps extends React.HTMLAttributes<HTMLSpanElement> {\n  /** A single phrase or a list cycled type -> pause -> delete -> next. */\n  phrases: string | string[]\n  /** Milliseconds per typed character. */\n  typingSpeed?: number\n  /** Milliseconds per deleted character. */\n  deletingSpeed?: number\n  /** Milliseconds a completed phrase holds before deleting. */\n  pause?: number\n  /** Milliseconds before the first character types. */\n  startDelay?: number\n  /** When false, stops after fully typing the last phrase (caret keeps blinking). */\n  loop?: boolean\n  showCaret?: boolean\n}\n\nconst Typewriter = React.forwardRef<HTMLSpanElement, TypewriterProps>(\n  (\n    {\n      className,\n      phrases,\n      typingSpeed = 45,\n      deletingSpeed = 25,\n      pause = 1600,\n      startDelay = 0,\n      loop = true,\n      showCaret = true,\n      ...props\n    },\n    ref,\n  ) => {\n    const list = React.useMemo(() => (Array.isArray(phrases) ? phrases : [phrases]), [phrases])\n    const srText = React.useMemo(() => list.join('. '), [list])\n\n    // Starts empty on server AND client first paint; sequencing begins in this\n    // effect only, so SSR markup and hydration output always match.\n    const [text, setText] = React.useState('')\n    const [reduced, setReduced] = React.useState(false)\n\n    React.useEffect(() => {\n      const isReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches\n      setReduced(isReduced)\n      if (isReduced) {\n        // Skip the animation entirely: render the phrase in full with a static caret.\n        setText(list[0] ?? '')\n        return\n      }\n\n      let timer: ReturnType<typeof setTimeout> | undefined\n      let index = 0\n      let chars = 0\n      let deleting = false\n\n      const clear = () => {\n        if (timer !== undefined) clearTimeout(timer)\n        timer = undefined\n      }\n\n      const schedule = (fn: () => void, delay: number) => {\n        clear()\n        timer = setTimeout(() => {\n          timer = undefined\n          fn()\n        }, delay)\n      }\n\n      const step = () => {\n        const current = list[index] ?? ''\n        if (!deleting) {\n          chars += 1\n          setText(current.slice(0, chars))\n          if (chars < current.length) {\n            schedule(step, typingSpeed)\n          } else if (loop || index < list.length - 1) {\n            schedule(() => {\n              deleting = true\n              step()\n            }, pause)\n          }\n          // loop=false on the last phrase: stop here; the caret keeps blinking.\n        } else {\n          chars -= 1\n          setText(current.slice(0, chars))\n          if (chars > 0) {\n            schedule(step, deletingSpeed)\n          } else {\n            deleting = false\n            index = (index + 1) % list.length\n            step()\n          }\n        }\n      }\n\n      if (startDelay > 0) schedule(step, startDelay)\n      else step()\n\n      return clear\n    }, [list, typingSpeed, deletingSpeed, pause, startDelay, loop])\n\n    return (\n      <span ref={ref} data-uipkge=\"\" data-slot=\"typewriter\" className={cn(className)} {...props}>\n        <span className=\"sr-only\">{srText}</span>\n        <span aria-hidden=\"true\" className=\"whitespace-pre-wrap\">\n          <span>{text}</span>\n          {showCaret && (\n            <span\n              className={cn(\n                'inline-block h-[1em] w-[0.5ch] bg-current align-baseline',\n                !reduced && 'animate-caret-blink',\n              )}\n            />\n          )}\n        </span>\n      </span>\n    )\n  },\n)\n\nTypewriter.displayName = 'Typewriter'\n\nexport { Typewriter }\n",
      "type": "registry:ui",
      "target": "~/components/ui/typewriter/Typewriter.tsx"
    },
    {
      "path": "packages/registry-react/components/typewriter/index.ts",
      "content": "export { Typewriter, type TypewriterProps } from './Typewriter'\n",
      "type": "registry:ui",
      "target": "~/components/ui/typewriter/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Types text character-by-character with a blinking caret, optionally cycling through multiple phrases (type -> pause -> delete -> next). Configurable typing/deleting speeds, completion pause, start delay, looping, and caret visibility. Renders empty on the server for hydration-safe SSR, restarts when `phrases` changes, and shows phrases in full under `prefers-reduced-motion`.",
  "categories": [
    "display",
    "motion"
  ]
}