{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"streaming-text","type":"registry:ui","title":"StreamingText","description":"Renders a growing string as a live token stream: the appended delta lands tinted and settles, with an optional block cursor while the response is in flight","categories":["ai-chat"],"registryDependencies":["https://fancy-ui.rama.app/r/internals.json","https://fancy-ui.rama.app/r/internals-dom.json","https://fancy-ui.rama.app/r/internals-markdown.json","https://fancy-ui.rama.app/r/utils.json"],"files":[{"path":"react/src/components/streaming-text/StreamingText.tsx","content":"\"use client\";\n\nimport { forwardRef, useEffect, useRef } from \"react\";\nimport type { CSSProperties } from \"react\";\nimport { cn } from \"../../utils\";\nimport { StreamText } from \"../../internals/StreamText\";\nimport { Markdown } from \"../../internals/markdown/Markdown\";\nimport \"./streaming-text.css\";\n\nexport interface StreamingTextProps {\n\t/**\n\t * The accumulated text so far — not the latest delta. Reassign it with a\n\t * longer string as chunks arrive and the growth is what animates.\n\t */\n\ttext: string;\n\t/** While true, a soft block cursor trails the last character. */\n\tstreaming?: boolean;\n\t/** Render the text as markdown instead of a tinted plain-text stream. */\n\tmarkdown?: boolean;\n\t/** How long a newly arrived chunk stays tinted, in ms. Plain mode only. */\n\tsettleMs?: number;\n\t/** Colour a chunk fades from, and the cursor's fill. Any CSS colour. */\n\ttintColor?: string;\n\t/** Called once when `streaming` goes from true to false. */\n\tonComplete?: () => void;\n\t/** Additional CSS classes */\n\tclassName?: string;\n}\n\n/**\n * Markdown renders block children, so a cursor appended after the document\n * would sit on its own line below it. It goes in through `trailingCursor`\n * instead, which puts it inside the last block's own inline run. Plain text\n * is already inline, so there it is simply rendered after the stream.\n */\nconst cursor = <span className=\"ft-streaming-cursor\" aria-hidden=\"true\" />;\n\nexport const StreamingText = forwardRef<HTMLSpanElement, StreamingTextProps>(function StreamingText(\n\t{ text, streaming = false, markdown = false, settleMs = 350, tintColor, onComplete, className },\n\tref,\n) {\n\t// Seeded from the initial prop so a component that mounts with streaming\n\t// already false — a replayed transcript, say — does not report completion\n\t// it never witnessed. Only the true → false edge fires.\n\tconst wasStreamingRef = useRef(streaming);\n\tuseEffect(() => {\n\t\tconst now = streaming;\n\t\tif (wasStreamingRef.current && !now) onComplete?.();\n\t\twasStreamingRef.current = now;\n\t}, [streaming, onComplete]);\n\n\tconst style = tintColor ? ({ [\"--ft-tint-color\" as string]: tintColor } as CSSProperties) : undefined;\n\n\treturn (\n\t\t<span\n\t\t\tref={ref}\n\t\t\tclassName={cn(\"ft-streaming-text\", markdown && \"ft-streaming-block\", className)}\n\t\t\tstyle={style}\n\t\t>\n\t\t\t{markdown ? (\n\t\t\t\t<Markdown text={text} trailingCursor={streaming ? cursor : undefined} />\n\t\t\t) : (\n\t\t\t\t<>\n\t\t\t\t\t<StreamText text={text} settleMs={settleMs} />\n\t\t\t\t\t{streaming ? cursor : null}\n\t\t\t\t</>\n\t\t\t)}\n\t\t</span>\n\t);\n});\n","type":"registry:ui","target":"@ui/fancy/components/streaming-text/StreamingText.tsx"},{"path":"react/src/components/streaming-text/index.ts","content":"\"use client\";\n\nexport { StreamingText } from \"./StreamingText\";\nexport type { StreamingTextProps } from \"./StreamingText\";\n","type":"registry:ui","target":"@ui/fancy/components/streaming-text/index.ts"},{"path":"react/src/components/streaming-text/streaming-text.css","content":".ft-streaming-text {\n\tdisplay: inline;\n}\n\n/*\n * Markdown renders block children, which need a block box to lay out in.\n */\n.ft-streaming-block {\n\tdisplay: block;\n}\n\n/*\n * Falls back to currentColor rather than the tint's own blue default: an\n * untinted stream should not grow a coloured cursor, but a tinted one reads\n * as a single gesture when the cursor matches what it is depositing.\n */\n.ft-streaming-cursor {\n\tdisplay: inline-block;\n\tinline-size: 0.5em;\n\tblock-size: 1em;\n\tmargin-inline-start: 0.12em;\n\tvertical-align: text-bottom;\n\tborder-radius: 1px;\n\tbackground: var(--ft-tint-color, currentColor);\n\topacity: 0.75;\n}\n\n/*\n * Reduced motion keeps the cursor as a steady block: it still marks where the\n * next character lands, it just stops flashing.\n */\n@media (prefers-reduced-motion: no-preference) {\n\t.ft-streaming-cursor {\n\t\tanimation: ft-streaming-blink 1.05s steps(1, end) infinite;\n\t}\n\n\t@keyframes ft-streaming-blink {\n\t\t0%,\n\t\t50% {\n\t\t\topacity: 0.75;\n\t\t}\n\t\t50.01%,\n\t\t100% {\n\t\t\topacity: 0;\n\t\t}\n\t}\n}\n","type":"registry:ui","target":"@ui/fancy/components/streaming-text/streaming-text.css"}]}