{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"c-message-7","type":"registry:block","title":"Streaming reply with a pending state","description":"One assistant row moving from thinking, to typing, to settled.","dependencies":[],"registryDependencies":["bubble","button","marker","message","spinner"],"files":[{"path":"c-message-7.tsx","type":"registry:block","content":"\"use client\"\n\nimport { useEffect, useState } from \"react\"\n\nimport { Bubble, BubbleContent } from \"@/components/ui/bubble\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Marker,\n  MarkerContent,\n  MarkerIcon,\n} from \"@/components/ui/marker\"\nimport {\n  Message,\n  MessageContent,\n  MessageFooter,\n} from \"@/components/ui/message\"\nimport { Spinner } from \"@/components/ui/spinner\"\n\nconst ANSWER =\n  \"The tax service began retrying every confirm call at 14:05, twice at 400ms each, so p95 on POST /api/checkout/confirm went from 240ms to 3.1s. The database was never the bottleneck: connection wait stayed under 5ms for the whole window. Rolling the tax client back to 1.8.3 at 14:41 returned p95 to 260ms within two minutes.\"\n\nconst PENDING_MS = 900\nconst STEP_MS = 24\nconst STEP_CHARS = 4\n\n/*\n * Announcing every chunk turns the reply into a stutter, so one live region\n * carries the two states worth interrupting for. It is a permanent child, not\n * swapped in with its text: a region inserted with its content is not read.\n */\nexport function Pattern() {\n  const [revealed, setRevealed] = useState(0)\n\n  useEffect(() => {\n    if (revealed >= ANSWER.length) return\n    /*\n     * Reduced motion gets the whole answer, not a faster crawl. Someone who\n     * asked for less movement wants the text, not the same effect rushed.\n     */\n    if (window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) {\n      setRevealed(ANSWER.length)\n      return\n    }\n    const id = window.setTimeout(\n      () => setRevealed((count) => Math.min(count + STEP_CHARS, ANSWER.length)),\n      revealed === 0 ? PENDING_MS : STEP_MS\n    )\n    return () => window.clearTimeout(id)\n  }, [revealed])\n\n  const done = revealed >= ANSWER.length\n\n  return (\n    <div className=\"mx-auto flex w-full max-w-2xl flex-col gap-6\">\n      <Message align=\"end\">\n        <MessageContent>\n          <Bubble variant=\"muted\">\n            <BubbleContent>\n              Why did eu-west checkout latency spike at 14:05?\n            </BubbleContent>\n          </Bubble>\n        </MessageContent>\n      </Message>\n\n      <Message>\n        <MessageContent>\n          <span role=\"status\" className=\"sr-only\">\n            {done ? \"Reply ready\" : revealed === 0 ? \"Generating reply\" : \"\"}\n          </span>\n          {/*\n            The bubble is mounted in both states rather than swapped out, so\n            aria-busy covers the wait and the footer keeps its ghost padding.\n          */}\n          <Bubble variant=\"ghost\" aria-busy={!done}>\n            <BubbleContent>\n              {revealed === 0 ? (\n                <Marker>\n                  <MarkerIcon>\n                    <Spinner />\n                  </MarkerIcon>\n                  <MarkerContent>\n                    <span className=\"animate-pulse motion-reduce:animate-none\">\n                      Reading the 14:00 to 14:10 trace...\n                    </span>\n                  </MarkerContent>\n                </Marker>\n              ) : (\n                <p className=\"whitespace-pre-wrap\">\n                  {ANSWER.slice(0, revealed)}\n                  {done ? null : (\n                    <span\n                      aria-hidden=\"true\"\n                      className=\"bg-foreground ms-0.5 inline-block h-[1em] w-0.5 translate-y-0.5\"\n                    />\n                  )}\n                </p>\n              )}\n            </BubbleContent>\n          </Bubble>\n          {/*\n            The footer stays mounted so Replay survives its own click. A button\n            that unmounts the moment it is activated drops focus to the body.\n          */}\n          <MessageFooter className=\"gap-2\">\n            <span className=\"tabular-nums\">14:45</span>\n            <Button\n              type=\"button\"\n              variant=\"ghost\"\n              size=\"xs\"\n              onClick={() => setRevealed(0)}\n            >\n              Replay\n            </Button>\n          </MessageFooter>\n        </MessageContent>\n      </Message>\n    </div>\n  )\n}","target":"components/examples/c-message-7.tsx"}],"meta":{"order":7,"gridSize":1}}