{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "typewriter",
  "title": "Typewriter",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-vue/components/typewriter/Typewriter.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'\nimport type { HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\n\ninterface Props {\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  class?: HTMLAttributes['class']\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  typingSpeed: 45,\n  deletingSpeed: 25,\n  pause: 1600,\n  startDelay: 0,\n  loop: true,\n  showCaret: true,\n})\n\nconst phraseList = computed(() => (Array.isArray(props.phrases) ? props.phrases : [props.phrases]))\nconst srText = computed(() => phraseList.value.join('. '))\n\n// Starts empty on server AND client first paint; sequencing begins in\n// onMounted only, so SSR markup and hydration output always match.\nconst text = ref('')\nconst reduced = ref(false)\n\nlet timer: ReturnType<typeof setTimeout> | null = null\n\nfunction clearTimer() {\n  if (timer !== null) {\n    clearTimeout(timer)\n    timer = null\n  }\n}\n\nfunction schedule(fn: () => void, delay: number) {\n  clearTimer()\n  timer = setTimeout(() => {\n    timer = null\n    fn()\n  }, delay)\n}\n\nfunction run() {\n  clearTimer()\n  const list = phraseList.value\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.value = current.slice(0, chars)\n      if (chars < current.length) {\n        schedule(step, props.typingSpeed)\n      } else if (props.loop || index < list.length - 1) {\n        schedule(() => {\n          deleting = true\n          step()\n        }, props.pause)\n      }\n      // loop=false on the last phrase: stop here; the caret keeps blinking.\n    } else {\n      chars -= 1\n      text.value = current.slice(0, chars)\n      if (chars > 0) {\n        schedule(step, props.deletingSpeed)\n      } else {\n        deleting = false\n        index = (index + 1) % list.length\n        step()\n      }\n    }\n  }\n\n  if (props.startDelay > 0) schedule(step, props.startDelay)\n  else step()\n}\n\nonMounted(() => {\n  reduced.value = window.matchMedia('(prefers-reduced-motion: reduce)').matches\n  if (reduced.value) {\n    // Skip the animation entirely: render the phrase in full with a static caret.\n    text.value = phraseList.value[0] ?? ''\n    return\n  }\n  run()\n})\n\n// Restart the sequence when the phrases prop changes.\nwatch(\n  () => props.phrases,\n  () => {\n    if (reduced.value) {\n      text.value = phraseList.value[0] ?? ''\n      return\n    }\n    run()\n  },\n)\n\nonBeforeUnmount(clearTimer)\n</script>\n\n<template>\n  <span data-uipkge data-slot=\"typewriter\" :class=\"cn(props.class)\">\n    <span class=\"sr-only\">{{ srText }}</span>\n    <span aria-hidden=\"true\" class=\"whitespace-pre-wrap\"\n      ><span>{{ text }}</span\n      ><span\n        v-if=\"props.showCaret\"\n        :class=\"['inline-block h-[1em] w-[0.5ch] bg-current align-baseline', !reduced && 'animate-caret-blink']\"\n    /></span>\n  </span>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/typewriter/Typewriter.vue"
    },
    {
      "path": "packages/registry-vue/components/typewriter/index.ts",
      "content": "export { default as Typewriter } from './Typewriter.vue'\n",
      "type": "registry:ui",
      "target": "~/app/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"
  ]
}