{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "typewriter",
  "title": "Typewriter",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/typewriter/typewriter.component.ts",
      "content": "import {\n  Component,\n  Input,\n  OnChanges,\n  OnDestroy,\n  SimpleChanges,\n  booleanAttribute,\n  signal,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { cn } from '@/lib/utils'\n\n/** Inputs that restart the sequence (React's effect dependency list). */\nconst SEQUENCE_INPUTS = ['phrases', 'typingSpeed', 'deletingSpeed', 'pause', 'startDelay', 'loop']\n\n/**\n * Types text character by character with a blinking caret (React `Typewriter`). A list of\n * phrases cycles type -> pause -> delete -> next; `loop=false` stops after the last phrase\n * (caret keeps blinking). Screen readers get the full text once via an sr-only copy.\n * Starts empty, so SSR markup and hydration match; prefers-reduced-motion renders the first\n * phrase in full with a static caret.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-typewriter, [ui-typewriter]',\n  standalone: true,\n  host: {\n    'data-uipkge': '',\n    'data-slot': 'typewriter',\n    '[class]': 'hostClass',\n  },\n  template: `<span class=\"sr-only\">{{ srText }}</span\n    ><span aria-hidden=\"true\" class=\"whitespace-pre-wrap\"\n      ><span>{{ text() }}</span>\n      @if (showCaret) {\n        <span [class]=\"caretClass\"></span>\n      }\n    </span>`,\n})\nexport class UiTypewriterComponent implements OnChanges, OnDestroy {\n  /** A single phrase or a list cycled type -> pause -> delete -> next. */\n  @Input({ required: true }) phrases: string | string[] = ''\n  /** Milliseconds per typed character. */\n  @Input() typingSpeed = 45\n  /** Milliseconds per deleted character. */\n  @Input() deletingSpeed = 25\n  /** Milliseconds a completed phrase holds before deleting. */\n  @Input() pause = 1600\n  /** Milliseconds before the first character types. */\n  @Input() startDelay = 0\n  /** When false, stops after fully typing the last phrase (caret keeps blinking). */\n  @Input({ transform: booleanAttribute }) loop = true\n  @Input({ transform: booleanAttribute }) showCaret = true\n  @Input('class') className?: string\n\n  // Signals: written from timers, which never schedule change detection in zoneless apps.\n  readonly text = signal('')\n  readonly reduced = signal(false)\n  private timer?: ReturnType<typeof setTimeout>\n\n  get list(): string[] {\n    return Array.isArray(this.phrases) ? this.phrases : [this.phrases]\n  }\n\n  get srText(): string {\n    return this.list.join('. ')\n  }\n\n  get hostClass(): string {\n    return cn(this.className)\n  }\n\n  get caretClass(): string {\n    return cn('inline-block h-[1em] w-[0.5ch] bg-current align-baseline', !this.reduced() && 'animate-caret-blink')\n  }\n\n  ngOnChanges(changes: SimpleChanges): void {\n    if (SEQUENCE_INPUTS.some((k) => k in changes)) this.start()\n  }\n\n  ngOnDestroy(): void {\n    this.clear()\n  }\n\n  private clear(): void {\n    if (this.timer !== undefined) clearTimeout(this.timer)\n    this.timer = undefined\n  }\n\n  private schedule(fn: () => void, delay: number): void {\n    this.clear()\n    this.timer = setTimeout(() => {\n      this.timer = undefined\n      fn()\n    }, delay)\n  }\n\n  private start(): void {\n    this.clear()\n    if (typeof window === 'undefined') return\n    const list = this.list\n    const isReduced = !!window.matchMedia?.('(prefers-reduced-motion: reduce)').matches\n    this.reduced.set(isReduced)\n    if (isReduced) {\n      // Skip the animation entirely: render the phrase in full with a static caret.\n      this.text.set(list[0] ?? '')\n      return\n    }\n\n    const { typingSpeed, deletingSpeed, pause, loop } = this\n    let index = 0\n    let chars = 0\n    let deleting = false\n\n    const step = (): void => {\n      const current = list[index] ?? ''\n      if (!deleting) {\n        chars += 1\n        this.text.set(current.slice(0, chars))\n        if (chars < current.length) {\n          this.schedule(step, typingSpeed)\n        } else if (loop || index < list.length - 1) {\n          this.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        this.text.set(current.slice(0, chars))\n        if (chars > 0) {\n          this.schedule(step, deletingSpeed)\n        } else {\n          deleting = false\n          index = (index + 1) % list.length\n          step()\n        }\n      }\n    }\n\n    if (this.startDelay > 0) this.schedule(step, this.startDelay)\n    else step()\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/typewriter/typewriter.component.ts"
    },
    {
      "path": "packages/registry-angular/components/typewriter/index.ts",
      "content": "export { UiTypewriterComponent } from './typewriter.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/typewriter/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Types text character-by-character with a blinking caret, optionally cycling through multiple phrases. Configurable speeds, pause, start delay, looping, and caret visibility. SSR-safe.",
  "categories": [
    "display",
    "motion"
  ]
}