{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ink/streaming-text",
  "title": "Streaming Text",
  "description": "Token-by-token streaming text with blinking cursor and optional typing animation",
  "dependencies": [
    "ink"
  ],
  "registryDependencies": [
    "https://termcn.dev/r/ink/terminal-text.json",
    "https://termcn.dev/r/ink/use-motion.json",
    "https://termcn.dev/r/ink/use-theme.json",
    "https://termcn.dev/r/ink/use-unicode.json"
  ],
  "files": [
    {
      "path": "registry/ui/streaming-text.tsx",
      "content": "import { useIsScreenReaderEnabled, Text } from \"ink\";\nimport React, { useEffect, useMemo, useRef, useState } from \"react\";\n\nimport { useMotion } from \"@/hooks/use-motion\";\nimport { useTheme } from \"@/hooks/use-theme\";\nimport { useUnicode } from \"@/hooks/use-unicode\";\nimport { splitGraphemes } from \"@/lib/terminal-text\";\n\nexport interface StreamingTextProps {\n  text?: string;\n  stream?: AsyncIterable<string>;\n  cursor?: boolean;\n  animate?: boolean;\n  speed?: number;\n  onComplete?: (fullText: string) => void;\n  cursorColor?: string;\n  isActive?: boolean;\n  \"aria-label\"?: string;\n}\n\nexport const StreamingText = ({\n  text: controlledText,\n  stream,\n  cursor = true,\n  animate = false,\n  speed = 30,\n  onComplete,\n  cursorColor,\n  isActive = true,\n  \"aria-label\": ariaLabel,\n}: StreamingTextProps) => {\n  const theme = useTheme();\n  const unicode = useUnicode();\n  const { reduced } = useMotion();\n  const isScreenReaderEnabled = useIsScreenReaderEnabled();\n  const [internalText, setInternalText] = useState(\"\");\n  const [isStreaming, setIsStreaming] = useState(false);\n  const [cursorVisible, setCursorVisible] = useState(true);\n  const [animatedIndex, setAnimatedIndex] = useState(0);\n  const onCompleteRef = useRef(onComplete);\n  const controlledGraphemes = useMemo(\n    () => splitGraphemes(controlledText ?? \"\"),\n    [controlledText]\n  );\n  const motionActive = isActive && !reduced && !isScreenReaderEnabled;\n\n  useEffect(() => {\n    onCompleteRef.current = onComplete;\n  }, [onComplete]);\n\n  useEffect(() => {\n    if (!cursor || !isStreaming || !motionActive) {\n      setCursorVisible(true);\n      return;\n    }\n    const id = setInterval(() => {\n      setCursorVisible((v) => !v);\n    }, 530);\n    return () => clearInterval(id);\n  }, [cursor, isStreaming, motionActive]);\n\n  useEffect(() => {\n    if (!stream) {\n      return;\n    }\n    let cancelled = false;\n    setInternalText(\"\");\n    setIsStreaming(true);\n\n    (async () => {\n      let full = \"\";\n      try {\n        for await (const chunk of stream) {\n          if (cancelled) {\n            break;\n          }\n          full += chunk;\n          if (!isScreenReaderEnabled || full.length % 64 < chunk.length) {\n            setInternalText(full);\n          }\n        }\n      } catch {\n        /* noop */\n      }\n      if (!cancelled) {\n        setInternalText(full);\n        setIsStreaming(false);\n        onCompleteRef.current?.(full);\n      }\n    })();\n\n    return () => {\n      cancelled = true;\n    };\n  }, [isScreenReaderEnabled, stream]);\n\n  useEffect(() => {\n    if (!animate || !controlledText || stream || !motionActive) {\n      setAnimatedIndex(controlledGraphemes.length);\n      return;\n    }\n    setAnimatedIndex(0);\n    setIsStreaming(true);\n    let idx = 0;\n    const id = setInterval(() => {\n      idx += 1;\n      setAnimatedIndex(idx);\n      if (idx >= controlledGraphemes.length) {\n        clearInterval(id);\n        setIsStreaming(false);\n        onCompleteRef.current?.(controlledText);\n      }\n    }, speed);\n    return () => clearInterval(id);\n  }, [\n    animate,\n    controlledGraphemes.length,\n    controlledText,\n    motionActive,\n    speed,\n    stream,\n  ]);\n\n  let displayText: string;\n  if (stream) {\n    displayText = internalText;\n  } else if (animate && controlledText && motionActive) {\n    displayText = controlledGraphemes.slice(0, animatedIndex).join(\"\");\n  } else {\n    displayText = controlledText ?? \"\";\n  }\n\n  const showCursor = cursor && isStreaming && cursorVisible;\n  const resolvedCursorColor = cursorColor ?? theme.colors.primary;\n\n  return (\n    <Text aria-label={ariaLabel ? `${ariaLabel}: ${displayText}` : undefined}>\n      {displayText}\n      {showCursor && (\n        <Text aria-hidden color={resolvedCursorColor}>\n          {unicode ? \"▌\" : \"|\"}\n        </Text>\n      )}\n    </Text>\n  );\n};\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "ai"
  ],
  "type": "registry:ui"
}