{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"text-generate","type":"registry:ui","title":"Text Generate","description":"Words stream in one by one out of a soft blur, like a model typing its answer, with an optional glowing caret.","dependencies":[],"registryDependencies":[],"files":[{"path":"registry/ui/text-generate.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { Fragment, useEffect, useRef, useState } from \"react\";\n\nconst css = `@keyframes text-generate-blink{0%,45%{opacity:1}55%,100%{opacity:0}}`;\n\nexport interface TextGenerateProps {\n  text: string;\n  /** Delay between words, in ms. */\n  stagger?: number;\n  /** Fade/unblur duration of each word, in ms. */\n  duration?: number;\n  /** Starting blur of each word, in px. */\n  blur?: number;\n  /** Show a caret that follows the stream and blinks when done. */\n  caret?: boolean;\n  /** Caret color. */\n  caretColor?: string;\n  className?: string;\n}\n\nexport function TextGenerate({\n  text,\n  stagger = 70,\n  duration = 700,\n  blur = 10,\n  caret = true,\n  caretColor = \"#a3e635\",\n  className,\n}: TextGenerateProps) {\n  const ref = useRef<HTMLParagraphElement>(null);\n  const words = text.split(/\\s+/).filter(Boolean);\n  const [count, setCount] = useState(0);\n  const [prevText, setPrevText] = useState(text);\n  if (prevText !== text) {\n    setPrevText(text);\n    setCount(0);\n  }\n\n  useEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    const total = text.split(/\\s+/).filter(Boolean).length;\n    if (window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) {\n      const t = setTimeout(() => setCount(total), 0);\n      return () => clearTimeout(t);\n    }\n    let timer: ReturnType<typeof setInterval> | undefined;\n    const io = new IntersectionObserver(\n      ([entry]) => {\n        if (!entry.isIntersecting || timer) return;\n        io.disconnect();\n        timer = setInterval(() => {\n          setCount((c) => {\n            if (c + 1 >= total) clearInterval(timer);\n            return Math.min(c + 1, total);\n          });\n        }, Math.max(stagger, 16));\n      },\n      { threshold: 0.3 },\n    );\n    io.observe(el);\n    return () => {\n      io.disconnect();\n      clearInterval(timer);\n    };\n  }, [text, stagger]);\n\n  const done = count >= words.length;\n  const caretEl = (\n    <span aria-hidden style={{ position: \"relative\", display: \"inline-block\", width: 0 }}>\n      <span\n        style={{\n          position: \"absolute\",\n          left: \"0.12em\",\n          bottom: \"-0.05em\",\n          width: \"0.09em\",\n          height: \"1em\",\n          borderRadius: 2,\n          background: caretColor,\n          boxShadow: `0 0 12px ${caretColor}`,\n          animation: done ? \"text-generate-blink 1s steps(1) infinite\" : undefined,\n        }}\n      />\n    </span>\n  );\n\n  return (\n    <p ref={ref} className={className}>\n      <style href=\"text-generate\" precedence=\"default\">\n        {css}\n      </style>\n      <span className=\"sr-only\">{text}</span>\n      {caret && count === 0 && caretEl}\n      {words.map((word, i) => {\n        const on = i < count;\n        return (\n          <Fragment key={i}>\n            <span\n              aria-hidden\n              style={{\n                display: \"inline-block\",\n                opacity: on ? 1 : 0,\n                filter: on ? \"blur(0px)\" : `blur(${blur}px)`,\n                transform: on ? \"none\" : \"translateY(0.15em)\",\n                transition: `opacity ${duration}ms ease, filter ${duration}ms ease, transform ${duration}ms cubic-bezier(.2,.7,.2,1)`,\n                willChange: on ? undefined : \"opacity, filter\",\n              }}\n            >\n              {word}\n            </span>\n            {caret && i === count - 1 && caretEl}\n            {i < words.length - 1 && \" \"}\n          </Fragment>\n        );\n      })}\n    </p>\n  );\n}\n"}]}