{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "typewriter",
  "title": "Typewriter",
  "description": "Framework-agnostic typewriter effect with multi-segment support, per-segment styling, viewport triggers, loops, and custom cursors",
  "dependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/blocks/animations/typewriter/components/elements/typewriter.tsx",
      "content": "\"use client\";\n\nimport React, { type JSX, useEffect, useMemo, useRef, useState } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport type TypewriterSegment =\n  | string\n  | {\n      text: string;\n      className?: string;\n      delay?: number;\n      speed?: number;\n    };\n\nexport type TypewriterProps = {\n  text: TypewriterSegment | TypewriterSegment[];\n  as?: React.ElementType;\n  className?: string;\n  speed?: number;\n  delay?: number;\n  startOnView?: boolean;\n  loop?: boolean;\n  loopDelay?: number;\n  cursor?: boolean;\n  cursorCharacter?: string;\n  cursorClassName?: string;\n  onComplete?: () => void;\n};\n\nconst DEFAULT_SPEED = 30;\n\nfunction normalize(text: TypewriterProps[\"text\"]) {\n  const segments = Array.isArray(text) ? text : [text];\n  return segments.map((segment) =>\n    typeof segment === \"string\" ? { text: segment } : segment,\n  );\n}\n\nfunction TypewriterBase({\n  text,\n  as: Component = \"span\",\n  className,\n  speed = DEFAULT_SPEED,\n  delay = 0,\n  startOnView = false,\n  loop = false,\n  loopDelay = 1500,\n  cursor = true,\n  cursorCharacter = \"|\",\n  cursorClassName,\n  onComplete,\n}: TypewriterProps) {\n  const segments = useMemo(() => normalize(text), [text]);\n  const containerRef = useRef<HTMLElement | null>(null);\n  const [started, setStarted] = useState(!startOnView);\n  const [segmentIndex, setSegmentIndex] = useState(0);\n  const [charIndex, setCharIndex] = useState(0);\n  const [done, setDone] = useState(false);\n\n  useEffect(() => {\n    if (!startOnView || started || !containerRef.current) return;\n    const node = containerRef.current;\n    const observer = new IntersectionObserver(\n      ([entry]) => {\n        if (entry?.isIntersecting) {\n          setStarted(true);\n          observer.disconnect();\n        }\n      },\n      { threshold: 0.1 },\n    );\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, [startOnView, started]);\n\n  useEffect(() => {\n    if (!started) return;\n    if (segmentIndex >= segments.length) {\n      setDone(true);\n      onComplete?.();\n      if (loop) {\n        const resetTimer = window.setTimeout(() => {\n          setDone(false);\n          setSegmentIndex(0);\n          setCharIndex(0);\n        }, loopDelay);\n        return () => window.clearTimeout(resetTimer);\n      }\n      return;\n    }\n\n    const current = segments[segmentIndex];\n    if (!current) return;\n    const currentSpeed = current.speed ?? speed;\n    const segmentDelay = current.delay ?? 0;\n    const isFirstChar = charIndex === 0;\n    const leadingDelay = isFirstChar\n      ? segmentIndex === 0\n        ? delay + segmentDelay\n        : segmentDelay\n      : 0;\n\n    if (charIndex < current.text.length) {\n      const wait = isFirstChar ? leadingDelay + currentSpeed : currentSpeed;\n      const timer = window.setTimeout(() => {\n        setCharIndex((idx) => idx + 1);\n      }, wait);\n      return () => window.clearTimeout(timer);\n    }\n\n    setSegmentIndex((idx) => idx + 1);\n    setCharIndex(0);\n    return;\n  }, [\n    started,\n    segmentIndex,\n    charIndex,\n    segments,\n    speed,\n    delay,\n    loop,\n    loopDelay,\n    onComplete,\n  ]);\n\n  const MotionComponent = Component as keyof JSX.IntrinsicElements;\n\n  const segmentsWithKeys = useMemo(\n    () =>\n      segments.map((segment, idx) => ({\n        ...segment,\n        key: `${idx}-${segment.text.slice(0, 24)}`,\n      })),\n    [segments],\n  );\n\n  const renderedSegments = segmentsWithKeys.map((segment, idx) => {\n    if (idx > segmentIndex) return null;\n    const value =\n      idx < segmentIndex ? segment.text : segment.text.slice(0, charIndex);\n    if (!value) return null;\n    return (\n      <span key={segment.key} className={cn(segment.className)}>\n        {value}\n      </span>\n    );\n  });\n\n  return React.createElement(\n    MotionComponent,\n    {\n      ref: containerRef as React.RefObject<HTMLElement>,\n      \"data-slot\": \"typewriter\",\n      className: cn(\"inline-block whitespace-pre-wrap\", className),\n      \"aria-live\": \"polite\",\n    },\n    <>\n      {renderedSegments}\n      {cursor && (\n        <span\n          aria-hidden=\"true\"\n          className={cn(\n            \"inline-block translate-y-[0.05em] animate-[typewriter-blink_1s_steps(1)_infinite]\",\n            done && !loop && \"opacity-0\",\n            cursorClassName,\n          )}\n          style={{\n            marginLeft: done ? 0 : \"0.05em\",\n          }}\n        >\n          {cursorCharacter}\n        </span>\n      )}\n      <style>{`\n        @keyframes typewriter-blink {\n          50% { opacity: 0; }\n        }\n      `}</style>\n    </>,\n  );\n}\n\nexport const Typewriter = React.memo(TypewriterBase);\n",
      "type": "registry:component"
    },
    {
      "path": "registry/default/blocks/animations/typewriter/routes/layout.tsx",
      "content": "import { Geist, Geist_Mono } from \"next/font/google\";\n\nconst geistSans = Geist({\n  variable: \"--font-geist-sans\",\n  subsets: [\"latin\"],\n});\n\nconst geistMono = Geist_Mono({\n  variable: \"--font-geist-mono\",\n  subsets: [\"latin\"],\n});\n\nexport default function RootLayout({\n  children,\n}: Readonly<{\n  children: React.ReactNode;\n}>) {\n  return (\n    <html lang=\"en\">\n      <body\n        className={`${geistSans.variable} ${geistMono.variable} antialiased`}\n      >\n        {children}\n      </body>\n    </html>\n  );\n}\n",
      "type": "registry:page",
      "target": "app/layout.tsx"
    },
    {
      "path": "registry/default/blocks/animations/typewriter/routes/page.tsx",
      "content": "import { Typewriter } from \"@/registry/default/blocks/animations/typewriter/components/elements/typewriter\";\n\nexport default function TypewriterPage() {\n  return (\n    <div className=\"min-h-screen bg-background text-foreground\">\n      <div className=\"container mx-auto px-4 py-16\">\n        <div className=\"max-w-3xl mx-auto space-y-16\">\n          <div className=\"text-center space-y-4\">\n            <h1 className=\"text-3xl font-bold\">Typewriter</h1>\n            <p className=\"text-muted-foreground\">\n              Framework-agnostic typewriter effect with multi-segment support,\n              viewport triggers, and custom cursors.\n            </p>\n          </div>\n\n          <section className=\"space-y-3\">\n            <p className=\"text-xs font-mono uppercase tracking-wider text-muted-foreground\">\n              Single line\n            </p>\n            <Typewriter\n              className=\"text-xl\"\n              text=\"Hola, soy Andenar. Te voy a ayudar a entender tu situación tributaria.\"\n            />\n          </section>\n\n          <section className=\"space-y-3\">\n            <p className=\"text-xs font-mono uppercase tracking-wider text-muted-foreground\">\n              Multi-segment with emphasis\n            </p>\n            <Typewriter\n              className=\"text-xl leading-relaxed\"\n              speed={24}\n              text={[\n                { text: \"Vamos a repasar \" },\n                {\n                  text: \"6 temas clave\",\n                  className: \"text-[#c2794a] font-medium\",\n                  speed: 60,\n                },\n                { text: \" para armar tu diagnóstico personalizado.\" },\n              ]}\n            />\n          </section>\n\n          <section className=\"space-y-3\">\n            <p className=\"text-xs font-mono uppercase tracking-wider text-muted-foreground\">\n              Loop with custom cursor\n            </p>\n            <Typewriter\n              className=\"text-2xl font-medium\"\n              loop\n              loopDelay={1800}\n              cursorCharacter=\"▌\"\n              text=\"streams · en · vivo\"\n            />\n          </section>\n\n          <section className=\"space-y-3\">\n            <p className=\"text-xs font-mono uppercase tracking-wider text-muted-foreground\">\n              Triggered on viewport entry\n            </p>\n            <div className=\"h-72 overflow-y-auto rounded-lg border p-6\">\n              <div className=\"h-[200%] flex flex-col justify-end\">\n                <Typewriter\n                  className=\"text-lg\"\n                  startOnView\n                  text=\"Este párrafo espera a estar visible antes de escribirse.\"\n                />\n              </div>\n            </div>\n          </section>\n\n          <section className=\"space-y-3\">\n            <p className=\"text-xs font-mono uppercase tracking-wider text-muted-foreground\">\n              As a heading\n            </p>\n            <Typewriter\n              as=\"h2\"\n              className=\"text-4xl font-semibold tracking-tight\"\n              speed={40}\n              text=\"Polymorphic element\"\n            />\n          </section>\n        </div>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:page",
      "target": "app/typewriter/page.tsx"
    }
  ],
  "docs": "Polymorphic Typewriter component. Supports a single string or an array of segments (each with its own className, speed, and delay), viewport-triggered start, looping, blinking or hidden cursor, and an onComplete callback. Zero external animation dependencies — uses plain React state + timers so it works anywhere React runs.",
  "categories": [
    "animations"
  ],
  "type": "registry:block"
}