{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "typewriter",
  "title": "Typewriter",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/typewriter/Typewriter.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface TypewriterProps extends 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    ref?: HTMLSpanElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { onDestroy, onMount, untrack } from 'svelte'\n  import { cn } from '$lib/utils'\n\n  let {\n    phrases,\n    typingSpeed = 45,\n    deletingSpeed = 25,\n    pause = 1600,\n    startDelay = 0,\n    loop = true,\n    showCaret = true,\n    class: className,\n    ref = $bindable(null),\n    ...restProps\n  }: TypewriterProps = $props()\n\n  const phraseList = $derived(Array.isArray(phrases) ? phrases : [phrases])\n  const srText = $derived(phraseList.join('. '))\n\n  // Starts empty on server AND client first paint; sequencing begins in\n  // onMount only, so SSR markup and hydration output always match.\n  let text = $state('')\n  let reduced = $state(false)\n\n  let timer: ReturnType<typeof setTimeout> | null = null\n\n  function clearTimer() {\n    if (timer !== null) {\n      clearTimeout(timer)\n      timer = null\n    }\n  }\n\n  function schedule(fn: () => void, delay: number) {\n    clearTimer()\n    timer = setTimeout(() => {\n      timer = null\n      fn()\n    }, delay)\n  }\n\n  function run() {\n    clearTimer()\n    const list = phraseList\n    if (list.length === 0) return\n\n    let index = 0\n    let chars = 0\n    let deleting = false\n\n    function step() {\n      const current = list[index] ?? ''\n      if (!deleting) {\n        chars += 1\n        text = 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        text = 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\n  onMount(() => {\n    reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches\n    if (reduced) {\n      // Skip the animation entirely: render the phrase in full with a static caret.\n      text = phraseList[0] ?? ''\n      return\n    }\n    run()\n  })\n\n  // Restart the sequence when the phrases prop changes. The identity guard\n  // skips the effect's initial run (onMount owns the first start).\n  let prevPhrases: string | string[] = untrack(() => phrases)\n  $effect(() => {\n    if (phrases === prevPhrases) return\n    prevPhrases = phrases\n    if (reduced) {\n      text = phraseList[0] ?? ''\n      return\n    }\n    run()\n  })\n\n  onDestroy(clearTimer)\n</script>\n\n<span bind:this={ref} data-uipkge data-slot=\"typewriter\" class={cn(className)} {...restProps}>\n  <span class=\"sr-only\">{srText}</span>\n  <span aria-hidden=\"true\" class=\"whitespace-pre-wrap\"\n    ><span>{text}</span>{#if showCaret}<span\n        class={cn('inline-block h-[1em] w-[0.5ch] bg-current align-baseline', !reduced && 'animate-caret-blink')}\n      ></span>{/if}</span\n  >\n</span>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/typewriter/Typewriter.svelte"
    },
    {
      "path": "packages/registry-svelte/components/typewriter/index.ts",
      "content": "export { default as Typewriter, type TypewriterProps } from './Typewriter.svelte'\n",
      "type": "registry:ui",
      "target": "~/src/lib/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"
  ]
}