{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "stream-text",
  "type": "registry:ui",
  "title": "Stream text",
  "description": "Streaming text: reveals characters quickly like a token stream, resolves the leading edge out of a soft blur, and keeps a caret that blinks once the text settles.",
  "categories": [
    "atoms"
  ],
  "registryDependencies": [
    "https://irisui.dev/r/foundation.json",
    "https://irisui.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "src/registry/components/stream-text/stream-text.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useLayoutEffect, useRef, useState } from \"react\";\nimport { cn } from \"../lib/utils\";\n\n/* ─────────────────────────────────────────────────────────\n * STREAM TEXT — reusable streaming primitive\n * Reveals characters quickly (fast, like a real token stream);\n * the leading edge resolves out of a soft blur, and the caret\n * stays solid while streaming, then blinks once the text\n * settles. Inherits typography from its context, so it drops\n * into any text surface (Selection Actions, chat, etc.).\n * ───────────────────────────────────────────────────────── */\n\nexport function StreamText({\n  text,\n  charsPerTick = 2,\n  tickMs = 9,\n  blurTail = 6,\n  caret = true,\n  className,\n  onProgress,\n  onDone,\n}: {\n  text: string;\n  /** characters revealed per tick — higher is faster */\n  charsPerTick?: number;\n  /** interval between reveals, ms */\n  tickMs?: number;\n  /** how many trailing characters carry the soft blur edge */\n  blurTail?: number;\n  /** render the caret (solid while streaming, blinks once idle) */\n  caret?: boolean;\n  className?: string;\n  /** fires each tick — useful for re-anchoring UI to reflowing text */\n  onProgress?: () => void;\n  /** fires once the full string is shown */\n  onDone?: () => void;\n}) {\n  /* progress belongs to one run (text and pace); a new run starts again from zero */\n  const run = `${charsPerTick}:${tickMs}:${text}`;\n  const [progress, setProgress] = useState({ run, count: 0 });\n  const count = progress.run === run ? progress.count : 0;\n  const onProgressRef = useRef(onProgress);\n  const onDoneRef = useRef(onDone);\n  useLayoutEffect(() => {\n    onProgressRef.current = onProgress;\n    onDoneRef.current = onDone;\n  });\n\n  useEffect(() => {\n    let i = 0;\n    const id = setInterval(() => {\n      i = Math.min(i + charsPerTick, text.length);\n      setProgress({ run, count: i });\n      onProgressRef.current?.();\n      if (i >= text.length) {\n        clearInterval(id);\n        onDoneRef.current?.();\n      }\n    }, tickMs);\n    return () => clearInterval(id);\n  }, [run, text, charsPerTick, tickMs]);\n\n  const streaming = count < text.length;\n  const shown = text.slice(0, count);\n  const split = streaming ? Math.max(0, shown.length - blurTail) : shown.length;\n\n  return (\n    <span className={className}>\n      {shown.slice(0, split)}\n      {split < shown.length && (\n        <span className=\"blur-[1.6px] [mask-image:linear-gradient(to_right,oklch(0_0_0)_20%,oklch(0_0_0/0.2))] motion-reduce:blur-none motion-reduce:[mask-image:none]\">\n          {shown.slice(split)}\n        </span>\n      )}\n      {caret && (\n        <span\n          aria-hidden\n          className={cn(\n            \"ml-[1.5px] inline-block h-[1.05em] w-0.5 translate-y-[-0.5px] rounded-[1px] bg-ink align-text-bottom\",\n            !streaming && \"animate-stream-text-caret-blink motion-reduce:animate-none\",\n          )}\n        />\n      )}\n    </span>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/iris/stream-text/stream-text.tsx"
    }
  ],
  "docs": "Docs and live preview: https://irisui.dev/components/stream-text",
  "meta": {
    "kind": "component",
    "group": "Text & motion",
    "export": "StreamText",
    "docs": "https://irisui.dev/components/stream-text"
  }
}
