{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "streaming-text",
  "title": "Streaming Text",
  "description": "A streamed answer with inline citations, sources, and follow-ups.",
  "dependencies": [
    "lucide-react"
  ],
  "files": [
    {
      "path": "components/spectrumui/blocks/ai-assistants/streaming-text.tsx",
      "content": "'use client';\n\nimport { CornerDownLeft } from 'lucide-react';\nimport { cn } from '@/lib/utils';\nimport type { Citation, SuggestedPrompt } from './types';\n\nconst KEYFRAMES = `\n@keyframes su-caret { 0%, 49% { opacity: 1 } 50%, 100% { opacity: 0 } }\n@keyframes su-msg-in { from { opacity: 0; transform: translateY(6px) } to { opacity: 1; transform: none } }\n`;\n\nexport type StreamingTextVariant = 'Answer' | 'Sources';\n\nexport interface StreamingTextProps {\n  text: string;\n  streaming?: boolean;\n  citations?: Citation[];\n  followUps?: SuggestedPrompt[];\n  onFollowUp?: (prompt: SuggestedPrompt) => void;\n  variant?: StreamingTextVariant;\n  className?: string;\n}\n\nexport function StreamingText({\n  text,\n  streaming = false,\n  citations = [],\n  followUps = [],\n  onFollowUp,\n  variant = 'Answer',\n  className,\n}: StreamingTextProps) {\n  const parts = text.split(/(\\*\\*[^*]+\\*\\*|\\[\\d+\\])/g).filter(Boolean);\n\n  return (\n    <div className={cn('w-full max-w-[520px] text-[13.5px]', className)}>\n      <style dangerouslySetInnerHTML={{ __html: KEYFRAMES }} />\n\n      <p aria-hidden={streaming || undefined} className=\"leading-[1.7] text-neutral-700 dark:text-neutral-300\">\n        {parts.map((part, index) => {\n          if (part.startsWith('**') && part.endsWith('**')) {\n            return (\n              <strong key={index} className=\"font-semibold text-neutral-900 dark:text-neutral-50\">\n                {part.slice(2, -2)}\n              </strong>\n            );\n          }\n          if (/^\\[\\d+\\]$/.test(part)) {\n            return (\n              <sup\n                key={index}\n                className=\"ml-0.5 inline-grid size-[14px] place-items-center rounded bg-black/[0.07] align-super font-mono text-[8.5px] font-semibold text-neutral-600 dark:bg-white/[0.1] dark:text-neutral-300\"\n              >\n                {part.slice(1, -1)}\n              </sup>\n            );\n          }\n          return <span key={index}>{part}</span>;\n        })}\n        {streaming && (\n          <span\n            aria-hidden\n            className=\"ml-0.5 inline-block h-[1.05em] w-[2px] translate-y-[0.15em] animate-[su-caret_1s_steps(1,end)_infinite] bg-current motion-reduce:animate-none\"\n          />\n        )}\n      </p>\n\n      {!streaming && citations.length > 0 && (\n        <div className=\"mt-3.5 motion-safe:animate-[su-msg-in_240ms_cubic-bezier(0.23,1,0.32,1)_both]\">\n          {variant === 'Sources' ? (\n            <ul className=\"space-y-1.5\">\n              {citations.map((citation) => (\n                <li\n                  key={citation.id}\n                  className=\"flex items-baseline gap-2 text-[12.5px] leading-[1.55]\"\n                >\n                  <span className=\"grid size-[15px] shrink-0 translate-y-0.5 place-items-center rounded bg-black/[0.06] font-mono text-[9px] font-semibold text-neutral-600 dark:bg-white/[0.08] dark:text-neutral-300\">\n                    {citation.index}\n                  </span>\n                  <span className=\"min-w-0\">\n                    <span className=\"font-medium text-neutral-800 dark:text-neutral-200\">\n                      {citation.title}\n                    </span>\n                    <span className=\"ml-1.5 font-mono text-[10.5px] text-neutral-400 dark:text-neutral-600\">\n                      {new URL(citation.url).hostname}\n                    </span>\n                  </span>\n                </li>\n              ))}\n            </ul>\n          ) : (\n            <div className=\"flex items-center gap-2\">\n              <span aria-hidden className=\"flex -space-x-1\">\n                {citations.slice(0, 3).map((citation) => (\n                  <span\n                    key={citation.id}\n                    className=\"grid size-4 place-items-center rounded-full border border-white bg-neutral-200 font-mono text-[8px] font-semibold text-neutral-600 dark:border-[#0B0B0D] dark:bg-neutral-700 dark:text-neutral-200\"\n                  >\n                    {citation.index}\n                  </span>\n                ))}\n              </span>\n              <span className=\"font-mono text-[11px] text-neutral-400 dark:text-neutral-500\">\n                {citations.length} source{citations.length === 1 ? '' : 's'}\n              </span>\n            </div>\n          )}\n        </div>\n      )}\n\n      {!streaming && followUps.length > 0 && (\n        <div className=\"mt-4\">\n          <p className=\"mb-1.5 font-mono text-[10.5px] font-medium uppercase tracking-[0.07em] text-neutral-400 dark:text-neutral-600\">\n            Follow-ups\n          </p>\n          <ul>\n            {followUps.map((prompt, index) => (\n              <li\n                key={prompt.id}\n                className=\"motion-safe:animate-[su-msg-in_240ms_cubic-bezier(0.23,1,0.32,1)_both]\"\n                style={{ animationDelay: `${Math.min(index, 4) * 50}ms` }}\n              >\n                <button\n                  type=\"button\"\n                  onClick={() => onFollowUp?.(prompt)}\n                  className=\"group flex w-full items-center gap-2 border-b border-black/[0.05] py-2 text-left text-[12.5px] text-neutral-600 transition-[color,transform] duration-150 last:border-b-0 hover:text-neutral-900 active:scale-[0.99] focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-neutral-400 dark:border-white/[0.06] dark:text-neutral-400 dark:hover:text-neutral-100\"\n                >\n                  <CornerDownLeft className=\"size-3 text-neutral-300 transition-[color,transform] duration-150 ease-[cubic-bezier(0.23,1,0.32,1)] group-hover:translate-x-0.5 group-hover:text-neutral-500 dark:text-neutral-600 dark:group-hover:text-neutral-400\" />\n                  {prompt.label}\n                </button>\n              </li>\n            ))}\n          </ul>\n        </div>\n      )}\n    </div>\n  );\n}\n\nexport default StreamingText;\n",
      "type": "registry:block",
      "target": "components/spectrumui/blocks/ai-assistants/streaming-text.tsx"
    },
    {
      "path": "components/spectrumui/blocks/ai-assistants/types.ts",
      "content": "export type MessageRole = 'user' | 'assistant' | 'system';\n\nexport type MessageState = 'streaming' | 'complete' | 'error';\n\nexport interface Attachment {\n  id: string;\n  name: string;\n  size: number;\n  type: string;\n  url?: string;\n  previewUrl?: string;\n}\n\nexport interface Citation {\n  id: string;\n  index: number;\n  url: string;\n  title: string;\n  snippet?: string;\n  favicon?: string;\n}\n\nexport type ToolCallStatus = 'pending' | 'running' | 'success' | 'error' | 'cancelled';\n\nexport interface ToolCall {\n  id: string;\n  name: string;\n  args?: Record<string, unknown>;\n  result?: string;\n  status: ToolCallStatus;\n  startedAt?: number;\n  completedAt?: number;\n  children?: ToolCall[];\n  parallel?: boolean;\n}\n\nexport interface ReasoningStep {\n  id: string;\n  content: string;\n}\n\nexport interface Reasoning {\n  steps: ReasoningStep[];\n  status: 'thinking' | 'complete';\n  durationMs?: number;\n}\n\nexport interface Message {\n  id: string;\n  role: MessageRole;\n  content: string;\n  createdAt?: number;\n  state?: MessageState;\n  attachments?: Attachment[];\n  citations?: Citation[];\n  toolCalls?: ToolCall[];\n  reasoning?: Reasoning;\n}\n\nexport interface Conversation {\n  id: string;\n  label: string;\n}\n\nexport interface ModelOption {\n  id: string;\n  name: string;\n  description?: string;\n  badge?: string;\n  disabled?: boolean;\n}\n\nexport interface SuggestedPrompt {\n  id: string;\n  label: string;\n  prompt?: string;\n}\n\nexport type MessageFeedback = 'positive' | 'negative';\n\nexport interface UsageState {\n  promptTokens: number;\n  completionTokens: number;\n  contextWindow: number;\n  estimatedCostUsd?: number;\n}\n",
      "type": "registry:file",
      "target": "components/spectrumui/blocks/ai-assistants/types.ts"
    }
  ],
  "type": "registry:block"
}
