{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "streaming-text",
  "title": "Streaming Text",
  "description": "Text that shows a blinking caret while a reply arrives, and can type out finished text at a set speed for demos and replays.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/tealmac/streaming-text.tsx",
      "content": "// Adapted from Ballmac UI (https://github.com/vamsiy78/ballmac-ui), MIT License, Copyright (c) 2026 Ballmac.\n\"use client\"\n\nimport * as React from \"react\"\nimport { motion, useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst noopSubscribe = () => () => {}\n\n/** useReducedMotion that returns false until hydration, so server and client markup match. */\nfunction useReducedMotionSafe() {\n  const reduce = useReducedMotion()\n  const hydrated = React.useSyncExternalStore(\n    noopSubscribe,\n    () => true,\n    () => false\n  )\n  return hydrated && !!reduce\n}\n\ntype StreamingCaretProps = React.ComponentProps<\"span\">\n\n/** The blinking block caret shown at the end of text that is still arriving. Static under reduced motion. */\nfunction StreamingCaret({ className, ...props }: StreamingCaretProps) {\n  const reduceMotion = useReducedMotionSafe()\n  const classes = cn(\n    \"ms-0.5 inline-block h-[1.1em] w-[0.5ch] min-w-1.5 translate-y-[0.15em] rounded-[1px] bg-foreground/80\",\n    className\n  )\n  if (reduceMotion) {\n    return (\n      <span\n        data-slot=\"streaming-caret\"\n        aria-hidden=\"true\"\n        className={classes}\n        {...props}\n      />\n    )\n  }\n  return (\n    <motion.span\n      data-slot=\"streaming-caret\"\n      aria-hidden=\"true\"\n      className={classes}\n      animate={{ opacity: [1, 1, 0, 0] }}\n      transition={{\n        duration: 1.06,\n        times: [0, 0.5, 0.5, 1],\n        repeat: Infinity,\n        ease: \"linear\",\n      }}\n      {...(props as React.ComponentProps<typeof motion.span>)}\n    />\n  )\n}\n\ntype StreamingTextProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** The text received so far. Append to it as chunks arrive; whitespace and newlines are preserved. */\n  text: string\n  /** True while more text is expected. Shows the caret and sets aria-busy. */\n  streaming?: boolean\n  /** Reveal text that is already complete at `speed` characters per second (for demos and replays). Skipped under reduced motion. */\n  animate?: boolean\n  /** Characters per second when `animate` is on. */\n  speed?: number\n  /** Show the caret while streaming or revealing. */\n  caret?: boolean\n  /** Called once `animate` has revealed the whole text. */\n  onAnimationComplete?: () => void\n}\n\nfunction StreamingText({\n  text,\n  streaming = false,\n  animate = false,\n  speed = 80,\n  caret = true,\n  onAnimationComplete,\n  className,\n  ...props\n}: StreamingTextProps) {\n  const reduceMotion = useReducedMotionSafe()\n  const revealing = animate && !reduceMotion\n  const [count, setCount] = React.useState(0)\n  const countRef = React.useRef(0)\n  const completeRef = React.useRef(onAnimationComplete)\n  React.useEffect(() => {\n    completeRef.current = onAnimationComplete\n  })\n\n  React.useEffect(() => {\n    if (!revealing) return\n    // Text was replaced by something shorter: start over.\n    if (countRef.current > text.length) countRef.current = 0\n    const from = countRef.current\n    if (from >= text.length) return\n    const startedAt = performance.now()\n    let frame = 0\n    const tick = (now: number) => {\n      const next = Math.min(\n        text.length,\n        from + Math.floor(((now - startedAt) / 1000) * speed)\n      )\n      if (next !== countRef.current) {\n        countRef.current = next\n        setCount(next)\n      }\n      if (next < text.length) frame = requestAnimationFrame(tick)\n      else completeRef.current?.()\n    }\n    frame = requestAnimationFrame(tick)\n    return () => cancelAnimationFrame(frame)\n  }, [revealing, text, speed])\n\n  const shown = revealing ? text.slice(0, count) : text\n  const busy = streaming || (revealing && count < text.length)\n\n  return (\n    <div\n      data-slot=\"streaming-text\"\n      data-streaming={busy || undefined}\n      aria-live=\"polite\"\n      aria-busy={busy}\n      className={cn(\"break-words whitespace-pre-wrap\", className)}\n      {...props}\n    >\n      {shown}\n      {busy && caret ? <StreamingCaret /> : null}\n    </div>\n  )\n}\n\nexport {\n  StreamingText,\n  StreamingCaret,\n  type StreamingTextProps,\n  type StreamingCaretProps,\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}