{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "stream-response",
  "title": "Stream Response",
  "description": "Streaming markdown with a block caret, inline citations and a source strip. Stop and regenerate built in.",
  "dependencies": [
    "streamdown"
  ],
  "registryDependencies": [
    "https://spotcheckui.com/r/spotcheck-base.json",
    "https://spotcheckui.com/r/button.json",
    "https://spotcheckui.com/r/use-stream.json"
  ],
  "files": [
    {
      "path": "registry/spotcheck/components/spotcheck/stream-response.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Streamdown } from \"streamdown\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/registry/spotcheck/components/spotcheck/button\"\nimport type { StreamSource, StreamStatus } from \"@/registry/spotcheck/hooks/use-stream\"\n\nconst CITE_PREFIX = \"#cite-\"\n\n/**\n * Turns [1] into a link Streamdown can render, leaving markdown links alone. Pass a scope when\n * several responses share a page, so each citation reaches its own source.\n */\nexport function linkCitations(text: string, scope?: string) {\n  const prefix = scope ? `${CITE_PREFIX}${scope}-` : CITE_PREFIX\n  return text.replace(/\\[(\\d+)\\](?!\\()/g, `[$1](${prefix}$1)`)\n}\n\nfunction useCiteScope() {\n  return React.useId().replace(/\\W/g, \"\")\n}\n\n/** Sentences that have finished, so screen readers aren't sent every token. */\nexport function finishedSentences(text: string) {\n  return text.match(/\\S[^\\n]*?[.!?]+(?=\\s|$)/g) ?? []\n}\n\nconst MARKDOWN = {\n  p: (props: React.ComponentProps<\"p\">) => <p className=\"my-2.5 first:mt-0\" {...props} />,\n  ul: (props: React.ComponentProps<\"ul\">) => <ul className=\"my-2.5 list-['+_'] pl-5\" {...props} />,\n  ol: (props: React.ComponentProps<\"ol\">) => <ol className=\"my-2.5 list-decimal pl-5\" {...props} />,\n  strong: (props: React.ComponentProps<\"strong\">) => <strong className=\"font-bold\" {...props} />,\n  code: (props: React.ComponentProps<\"code\">) => (\n    <code className=\"bg-wash px-1 font-(family-name:--f-mono) text-[.9em]\" {...props} />\n  ),\n  pre: (props: React.ComponentProps<\"pre\">) => (\n    <pre\n      tabIndex={0}\n      className=\"bg-ink text-paper my-2.5 overflow-x-auto p-3 font-(family-name:--f-mono) text-[12.5px] [&_code]:bg-transparent [&_code]:p-0\"\n      {...props}\n    />\n  ),\n  a: ({ href = \"\", children, ...props }: React.ComponentProps<\"a\">) =>\n    href.startsWith(CITE_PREFIX) ? (\n      <a\n        href={href}\n        aria-label={`Source ${href.split(\"-\").at(-1)}`}\n        className=\"bg-ink text-paper hover:bg-accent-ink hover:text-on-accent spotcheck-focus mx-0.5 inline-flex h-[1.25em] min-w-[1.4em] items-center justify-center px-1 align-[2px] font-(family-name:--f-mono) text-[10.5px] no-underline\"\n      >\n        {href.split(\"-\").at(-1)}\n      </a>\n    ) : (\n      <a href={href} className=\"text-accent-ink underline underline-offset-2\" {...props}>\n        {children}\n      </a>\n    ),\n}\n\ntype StreamMarkdownProps = {\n  text: string\n  scope: string\n  streaming?: boolean\n  className?: string\n  children?: React.ReactNode\n}\n\nfunction StreamMarkdown({ text, scope, streaming = false, className, children }: StreamMarkdownProps) {\n  return (\n    <div aria-busy={streaming} className={cn(\"max-w-[62ch] text-base leading-relaxed text-pretty\", className)}>\n      <Streamdown\n        mode={streaming ? \"streaming\" : \"static\"}\n        controls={false}\n        linkSafety={{ enabled: false }}\n        components={MARKDOWN}\n      >\n        {linkCitations(text, scope)}\n      </Streamdown>\n      {children}\n    </div>\n  )\n}\n\nfunction StreamSources({ sources, scope }: { sources: StreamSource[]; scope: string }) {\n  if (sources.length === 0) return null\n\n  return (\n    <ol className=\"border-ink flex flex-wrap border-(length:--rule)\">\n      {sources.map((source, index) => (\n        <li\n          key={source.id}\n          id={`cite-${scope}-${index + 1}`}\n          className=\"border-ink min-w-0 flex-[1_1_140px] border-r-(length:--rule) px-2.5 py-1.5 text-xs last:border-r-0\"\n        >\n          <b className=\"text-mute block font-(family-name:--f-mono) text-[10.5px]\">[{index + 1}]</b>\n          {source.url ? (\n            <a href={source.url} className=\"spotcheck-focus underline underline-offset-2\">\n              {source.title}\n            </a>\n          ) : (\n            source.title\n          )}\n        </li>\n      ))}\n    </ol>\n  )\n}\n\n/** A finished response with nothing live about it, for showing saved replies. */\nexport function SavedResponse({ text, sources = [], className }: { text: string; sources?: StreamSource[]; className?: string }) {\n  const scope = useCiteScope()\n\n  return (\n    <div className={cn(\"flex flex-col gap-3.5 font-(family-name:--f-body)\", className)}>\n      <StreamMarkdown text={text} scope={scope} />\n      <StreamSources sources={sources} scope={scope} />\n    </div>\n  )\n}\n\ntype StreamResponseProps = {\n  /** The text so far. Markdown is rendered as it arrives, and [1] becomes a citation. */\n  text: string\n  sources?: StreamSource[]\n  status: StreamStatus\n  /** Shows a Stop button while streaming. */\n  onStop?: () => void\n  /** Shows a Regenerate button once it's finished. */\n  onRegenerate?: () => void\n  /** Shown if the stream fails. */\n  error?: string | null\n  className?: string\n}\n\nexport function StreamResponse({ text, sources = [], status, onStop, onRegenerate, error, className }: StreamResponseProps) {\n  const streaming = status === \"streaming\"\n  const spoken = streaming ? finishedSentences(text).at(-1) : \"\"\n  const scope = useCiteScope()\n\n  return (\n    <div className={cn(\"flex flex-col gap-3.5 font-(family-name:--f-body)\", className)}>\n      <StreamMarkdown text={text} scope={scope} streaming={streaming} className=\"min-h-[5em]\">\n        {streaming && (\n          <span\n            aria-hidden\n            data-testid=\"stream-caret\"\n            className=\"bg-accent-ink inline-block h-[1.05em] w-[.55em] align-[-2px] motion-safe:[animation:spotcheck-blink_1s_steps(1)_infinite]\"\n          />\n        )}\n      </StreamMarkdown>\n      {/* One sentence at a time, because announcing tokens makes a screen reader unusable. */}\n      <div role=\"status\" className=\"sr-only\">\n        {streaming ? spoken : status === \"done\" ? \"Response complete.\" : \"\"}\n      </div>\n      {error && (\n        <p role=\"alert\" className=\"text-bad font-(family-name:--f-mono) text-xs\">\n          {error}\n        </p>\n      )}\n      <StreamSources sources={sources} scope={scope} />\n      <div className=\"flex flex-wrap items-center gap-4\">\n        {streaming\n          ? onStop && <Button onClick={onStop}>Stop</Button>\n          : onRegenerate && (\n              <Button variant=\"accent\" onClick={onRegenerate}>\n                Regenerate\n              </Button>\n            )}\n        <span className=\"spotcheck-label\" data-testid=\"stream-status\">\n          {status}\n        </span>\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "categories": [
    "ai"
  ],
  "type": "registry:component"
}