{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "use-typewriter",
  "title": "useTypewriter",
  "description": "A typewriter hook that types, holds and deletes phrases on a loop, plus a prefers-reduced-motion hook.",
  "files": [
    {
      "path": "app/registry/use-typewriter/use-typewriter.ts",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nexport type TypewriterPhase =\n  | \"idle\"\n  | \"typing\"\n  | \"holding\"\n  | \"deleting\"\n  | \"waiting\";\n\nexport interface UseTypewriterOptions {\n  /** ms per typed character. */\n  typeMs?: number;\n  /** ms per deleted character. */\n  deleteMs?: number;\n  /** ms the finished phrase stays on screen. */\n  holdMs?: number;\n  /** ms of empty pause between phrases. */\n  gapMs?: number;\n  /** ms before the first phrase starts. */\n  startDelayMs?: number;\n  /** Pause the animation (e.g. while offscreen). */\n  enabled?: boolean;\n  /** Humanize typing speed with a little randomness. */\n  jitter?: boolean;\n}\n\nexport function usePrefersReducedMotion() {\n  const [reduced, setReduced] = React.useState(false);\n  React.useEffect(() => {\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    setReduced(mq.matches);\n    const onChange = (event: MediaQueryListEvent) => setReduced(event.matches);\n    mq.addEventListener(\"change\", onChange);\n    return () => mq.removeEventListener(\"change\", onChange);\n  }, []);\n  return reduced;\n}\n\n/**\n * Cycles through `phrases`, typing each in, holding it, then deleting it.\n * Respects prefers-reduced-motion (shows the first phrase statically).\n */\nexport function useTypewriter(\n  phrases: string[],\n  {\n    typeMs = 70,\n    deleteMs = 28,\n    holdMs = 1800,\n    gapMs = 450,\n    startDelayMs = 400,\n    enabled = true,\n    jitter = true,\n  }: UseTypewriterOptions = {},\n) {\n  const [text, setText] = React.useState(\"\");\n  const [phase, setPhase] = React.useState<TypewriterPhase>(\"idle\");\n  const reduced = usePrefersReducedMotion();\n  const phrasesKey = phrases.join(\"\u0000\");\n  const phrasesRef = React.useRef(phrases);\n  phrasesRef.current = phrases;\n\n  React.useEffect(() => {\n    const list = phrasesRef.current;\n    if (!enabled || list.length === 0) return;\n    if (reduced) {\n      setText(list[0]);\n      setPhase(\"idle\");\n      return;\n    }\n\n    let cancelled = false;\n    const sleep = (ms: number) =>\n      new Promise<void>((resolve) => setTimeout(resolve, ms));\n    const tick = (base: number) =>\n      jitter ? base * (0.6 + Math.random() * 0.8) : base;\n\n    (async () => {\n      await sleep(startDelayMs);\n      let index = 0;\n      while (!cancelled) {\n        const phrase = list[index % list.length];\n        setPhase(\"typing\");\n        for (let i = 1; i <= phrase.length; i++) {\n          if (cancelled) return;\n          setText(phrase.slice(0, i));\n          await sleep(tick(typeMs));\n        }\n        setPhase(\"holding\");\n        await sleep(holdMs);\n        if (cancelled) return;\n        setPhase(\"deleting\");\n        for (let i = phrase.length - 1; i >= 0; i--) {\n          if (cancelled) return;\n          setText(phrase.slice(0, i));\n          await sleep(deleteMs);\n        }\n        setPhase(\"waiting\");\n        await sleep(gapMs);\n        index++;\n      }\n    })();\n\n    return () => {\n      cancelled = true;\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [enabled, reduced, phrasesKey, typeMs, deleteMs, holdMs, gapMs, startDelayMs, jitter]);\n\n  return { text, phase };\n}\n",
      "type": "registry:hook",
      "target": "components/spectrumui/use-typewriter.ts"
    }
  ],
  "type": "registry:hook"
}
