{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"streaming-response","type":"registry:component","title":"Streaming Response","description":"A stable response surface with completion actions, rendered content, and an expandable source summary.","author":"Saurabh <saurabh10102@gmail.com>","dependencies":["clsx","lucide-react","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/agents/streaming-response.tsx","type":"registry:component","target":"@components/agents/streaming-response.tsx","content":"\"use client\";\n// beui.dev/components/agents/streaming-response\n\nimport {\n  Check,\n  ChevronDown,\n  Copy,\n  RotateCcw,\n  ThumbsDown,\n  ThumbsUp,\n} from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport {\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n} from \"react\";\nimport {\n  type CitationItem,\n  CitationList,\n  CitationStack,\n} from \"@/components/agents/citations\";\nimport { AgentDisclosure } from \"@/components/agents/agent-disclosure\";\nimport { EASE_OUT, SPRING_PRESS, SPRING_SWAP } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport type StreamingResponseStatus = \"streaming\" | \"complete\" | \"error\";\nexport type StreamingResponseFeedback = \"up\" | \"down\" | null;\n\nexport interface StreamingResponseProps {\n  /** Rendered response content. Pass plain text or the output of a Markdown renderer. */\n  children: ReactNode;\n  status?: StreamingResponseStatus;\n  /** Plain-text value copied by the built-in copy action. */\n  copyText?: string;\n  /** Overrides the built-in clipboard action. */\n  onCopy?: () => void | Promise<void>;\n  onRetry?: () => void;\n  /** Optional sources shown as a compact footer disclosure after streaming. */\n  sources?: CitationItem[];\n  sourcesOpen?: boolean;\n  defaultSourcesOpen?: boolean;\n  onSourcesOpenChange?: (open: boolean) => void;\n  sourceIdPrefix?: string;\n  feedback?: StreamingResponseFeedback;\n  defaultFeedback?: StreamingResponseFeedback;\n  onFeedbackChange?: (feedback: StreamingResponseFeedback) => void;\n  /** Set false when a surrounding conversation log announces streamed text. */\n  announce?: boolean;\n  /** Hides the built-in completion actions without changing response status. */\n  showActions?: boolean;\n  className?: string;\n  contentClassName?: string;\n  actionsClassName?: string;\n}\n\nfunction ResponseAction({\n  label,\n  active = false,\n  onClick,\n  children,\n}: {\n  label: string;\n  active?: boolean;\n  onClick: () => void;\n  children: ReactNode;\n}) {\n  const reduce = useReducedMotion() ?? false;\n\n  return (\n    <motion.button\n      type=\"button\"\n      aria-label={label}\n      title={label}\n      aria-pressed={label === \"Helpful\" || label === \"Not helpful\" ? active : undefined}\n      onClick={onClick}\n      whileTap={reduce ? undefined : { scale: 0.9 }}\n      transition={SPRING_PRESS}\n      className={cn(\n        \"grid size-7 place-items-center rounded-md text-muted-foreground outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\",\n        active && \"bg-muted text-foreground\",\n      )}\n    >\n      {children}\n    </motion.button>\n  );\n}\n\nexport function StreamingResponse({\n  children,\n  status = \"streaming\",\n  copyText,\n  onCopy,\n  onRetry,\n  sources = [],\n  sourcesOpen,\n  defaultSourcesOpen = false,\n  onSourcesOpenChange,\n  sourceIdPrefix,\n  feedback,\n  defaultFeedback = null,\n  onFeedbackChange,\n  announce = true,\n  showActions = true,\n  className,\n  contentClassName,\n  actionsClassName,\n}: StreamingResponseProps) {\n  const reduce = useReducedMotion() ?? false;\n  const baseId = useId();\n  const [copied, setCopied] = useState(false);\n  const [internalFeedback, setInternalFeedback] =\n    useState<StreamingResponseFeedback>(defaultFeedback);\n  const [internalSourcesOpen, setInternalSourcesOpen] =\n    useState(defaultSourcesOpen);\n  const copyTimer = useRef<number | undefined>(undefined);\n  const currentFeedback = feedback ?? internalFeedback;\n  const currentSourcesOpen = sourcesOpen ?? internalSourcesOpen;\n  const streaming = status === \"streaming\";\n  const complete = status === \"complete\";\n  const canCopy = Boolean(copyText || onCopy);\n  const hasSources = sources.length > 0;\n  const shouldShowActions =\n    showActions && !streaming && (canCopy || onRetry || complete || hasSources);\n  const sourcesContentId = `${baseId}-sources`;\n  const resolvedSourcePrefix =\n    sourceIdPrefix ?? `response-source-${baseId.replace(/:/g, \"\")}`;\n\n  useEffect(\n    () => () => {\n      if (copyTimer.current) window.clearTimeout(copyTimer.current);\n    },\n    [],\n  );\n\n  const handleCopy = useCallback(async () => {\n    if (onCopy) await onCopy();\n    else if (copyText) await navigator.clipboard?.writeText(copyText);\n\n    setCopied(true);\n    if (copyTimer.current) window.clearTimeout(copyTimer.current);\n    copyTimer.current = window.setTimeout(() => setCopied(false), 1600);\n  }, [copyText, onCopy]);\n\n  const setFeedback = (next: Exclude<StreamingResponseFeedback, null>) => {\n    const value = currentFeedback === next ? null : next;\n    if (feedback === undefined) setInternalFeedback(value);\n    onFeedbackChange?.(value);\n  };\n\n  const setSourcesOpen = useCallback(\n    (next: boolean) => {\n      if (sourcesOpen === undefined) setInternalSourcesOpen(next);\n      onSourcesOpenChange?.(next);\n    },\n    [onSourcesOpenChange, sourcesOpen],\n  );\n\n  return (\n    <div\n      data-state={status}\n      aria-busy={streaming}\n      className={cn(\"w-full\", className)}\n    >\n      <div\n        aria-live={announce ? \"polite\" : \"off\"}\n        className={cn(\n          \"text-sm leading-6 text-foreground/90 [&_a]:font-medium [&_a]:underline [&_a]:underline-offset-4 [&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-[0.9em] [&_ol]:my-3 [&_ol]:list-decimal [&_ol]:space-y-1 [&_ol]:pl-5 [&_p+p]:mt-3 [&_pre]:my-3 [&_pre]:overflow-x-auto [&_pre]:rounded-xl [&_pre]:border [&_pre]:border-border [&_pre]:bg-muted/45 [&_pre]:p-3 [&_pre_code]:bg-transparent [&_pre_code]:p-0 [&_ul]:my-3 [&_ul]:list-disc [&_ul]:space-y-1 [&_ul]:pl-5\",\n          contentClassName,\n        )}\n      >\n        {children}\n      </div>\n\n      <AnimatePresence initial={false}>\n        {shouldShowActions ? (\n          <motion.div\n            initial={reduce ? { opacity: 0 } : { opacity: 0, y: 4 }}\n            animate={{ opacity: 1, y: 0 }}\n            exit={{ opacity: 0 }}\n            transition={{ duration: reduce ? 0.12 : 0.22, ease: EASE_OUT }}\n            className=\"mt-3\"\n          >\n            <div className={cn(\"flex items-center gap-0.5\", actionsClassName)}>\n              {canCopy ? (\n                <ResponseAction\n                  label={copied ? \"Copied\" : \"Copy response\"}\n                  onClick={handleCopy}\n                >\n                  {copied ? (\n                    <Check className=\"size-3.5\" />\n                  ) : (\n                    <Copy className=\"size-3.5\" />\n                  )}\n                </ResponseAction>\n              ) : null}\n              {onRetry ? (\n                <ResponseAction label=\"Retry response\" onClick={onRetry}>\n                  <RotateCcw className=\"size-3.5\" />\n                </ResponseAction>\n              ) : null}\n              {complete ? (\n                <>\n                  <ResponseAction\n                    label=\"Helpful\"\n                    active={currentFeedback === \"up\"}\n                    onClick={() => setFeedback(\"up\")}\n                  >\n                    <ThumbsUp className=\"size-3.5\" />\n                  </ResponseAction>\n                  <ResponseAction\n                    label=\"Not helpful\"\n                    active={currentFeedback === \"down\"}\n                    onClick={() => setFeedback(\"down\")}\n                  >\n                    <ThumbsDown className=\"size-3.5\" />\n                  </ResponseAction>\n                </>\n              ) : null}\n              {hasSources ? (\n                <button\n                  type=\"button\"\n                  aria-expanded={currentSourcesOpen}\n                  aria-controls={sourcesContentId}\n                  onClick={() => setSourcesOpen(!currentSourcesOpen)}\n                  className=\"group ml-1 inline-flex min-h-7 items-center gap-2 rounded-md px-1.5 text-xs text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n                >\n                  <CitationStack citations={sources} />\n                  <span className=\"tabular-nums\">\n                    {sources.length} {sources.length === 1 ? \"source\" : \"sources\"}\n                  </span>\n                  <motion.span\n                    aria-hidden=\"true\"\n                    animate={{ rotate: currentSourcesOpen ? 180 : 0 }}\n                    transition={reduce ? { duration: 0 } : SPRING_SWAP}\n                    className=\"text-muted-foreground/50 group-hover:text-muted-foreground\"\n                  >\n                    <ChevronDown className=\"size-3\" />\n                  </motion.span>\n                </button>\n              ) : null}\n            </div>\n\n            {hasSources ? (\n              <AgentDisclosure\n                id={sourcesContentId}\n                open={currentSourcesOpen}\n              >\n                <CitationList\n                  citations={sources}\n                  idPrefix={resolvedSourcePrefix}\n                  className=\"mt-2 rounded-xl bg-muted p-2\"\n                />\n              </AgentDisclosure>\n            ) : null}\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  );\n}\n"},{"path":"components/agents/agent-disclosure.tsx","type":"registry:component","target":"@components/agents/agent-disclosure.tsx","content":"\"use client\";\n\nimport { motion, type HTMLMotionProps, useReducedMotion } from \"motion/react\";\nimport type { CSSProperties } from \"react\";\nimport { EASE_OUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface AgentDisclosureProps\n  extends Omit<HTMLMotionProps<\"div\">, \"animate\" | \"initial\"> {\n  open: boolean;\n  openHeight?: CSSProperties[\"height\"];\n}\n\n/** Shared transform-only reveal for collapsible agent content. */\nexport function AgentDisclosure({\n  open,\n  openHeight = \"auto\",\n  className,\n  style,\n  transition,\n  ...props\n}: AgentDisclosureProps) {\n  const reduce = useReducedMotion() ?? false;\n\n  return (\n    <motion.div\n      {...props}\n      aria-hidden={!open}\n      inert={!open}\n      initial={false}\n      animate={\n        reduce\n          ? { opacity: open ? 1 : 0 }\n          : {\n              opacity: open ? 1 : 0,\n              clipPath: open ? \"inset(0 0 0% 0)\" : \"inset(0 0 100% 0)\",\n              y: open ? 0 : -4,\n            }\n      }\n      transition={\n        transition ?? {\n          duration: reduce ? 0 : open ? 0.22 : 0.14,\n          ease: EASE_OUT,\n        }\n      }\n      className={cn(\"overflow-hidden\", className)}\n      style={{\n        ...style,\n        height: open ? openHeight : 0,\n        pointerEvents: open ? undefined : \"none\",\n        transformOrigin: \"top\",\n      }}\n    />\n  );\n}\n"},{"path":"components/agents/citations.tsx","type":"registry:component","target":"@components/agents/citations.tsx","content":"\"use client\";\n\nimport { BookOpenText, ChevronDown, ExternalLink, Globe2 } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport {\n  type ReactNode,\n  useCallback,\n  useId,\n  useState,\n} from \"react\";\nimport { AgentDisclosure } from \"@/components/agents/agent-disclosure\";\nimport { EASE_OUT, SPRING_LAYOUT, SPRING_SWAP } from \"@/lib/ease\";\nimport { getFaviconUrl } from \"@/lib/favicon\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface CitationItem {\n  id: string;\n  title: ReactNode;\n  domain?: ReactNode;\n  url?: string;\n}\n\nexport interface CitationsProps {\n  citations: CitationItem[];\n  title?: ReactNode;\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  idPrefix?: string;\n  className?: string;\n}\n\nexport interface CitationProps {\n  citationId: string;\n  index: number;\n  /** Must match the related Citations idPrefix. */\n  idPrefix: string;\n  className?: string;\n}\n\nexport interface CitationListProps {\n  citations: CitationItem[];\n  idPrefix?: string;\n  className?: string;\n}\n\nexport interface CitationStackProps {\n  citations: CitationItem[];\n  limit?: number;\n  className?: string;\n}\n\nfunction citationTargetId(prefix: string, citationId: string) {\n  return `${prefix}-${citationId.replace(/[^a-zA-Z0-9_-]/g, \"-\")}`;\n}\n\nexport function Citation({\n  citationId,\n  index,\n  idPrefix,\n  className,\n}: CitationProps) {\n  return (\n    <a\n      href={`#${citationTargetId(idPrefix, citationId)}`}\n      aria-label={`View citation ${index}`}\n      className={cn(\n        \"mx-0.5 inline-flex min-w-4 -translate-y-0.5 items-center justify-center rounded-md bg-muted/60 px-1 py-0.5 text-[10px] font-semibold leading-none text-muted-foreground no-underline outline-none transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\",\n        className,\n      )}\n    >\n      {index}\n    </a>\n  );\n}\n\nexport function CitationFavicon({\n  url,\n  className,\n}: {\n  url?: string;\n  className?: string;\n}) {\n  const favicon = url ? getFaviconUrl(url) : null;\n  const [failedUrl, setFailedUrl] = useState<string | null>(null);\n\n  return (\n    <span\n      aria-hidden=\"true\"\n      className={cn(\n        \"grid size-5 shrink-0 place-items-center text-muted-foreground\",\n        className,\n      )}\n    >\n      {favicon && failedUrl !== favicon ? (\n        // biome-ignore lint/performance/noImgElement: Dynamic cross-site favicons keep this framework-agnostic registry component portable.\n        <img\n          src={favicon}\n          alt=\"\"\n          width={16}\n          height={16}\n          referrerPolicy=\"no-referrer\"\n          onError={() => setFailedUrl(favicon)}\n          className=\"size-4 rounded-sm object-contain\"\n        />\n      ) : (\n        <Globe2 className=\"size-3.5\" />\n      )}\n    </span>\n  );\n}\n\nexport function CitationStack({\n  citations,\n  limit = 3,\n  className,\n}: CitationStackProps) {\n  return (\n    <span\n      aria-hidden=\"true\"\n      className={cn(\"flex -space-x-1.5\", className)}\n    >\n      {citations.slice(0, limit).map((citation) => (\n        <CitationFavicon\n          key={citation.id}\n          url={citation.url}\n          className=\"size-6 rounded-full bg-background ring-2 ring-background\"\n        />\n      ))}\n    </span>\n  );\n}\n\nfunction CitationRow({\n  citation,\n  index,\n  idPrefix,\n}: {\n  citation: CitationItem;\n  index: number;\n  idPrefix: string;\n}) {\n  const content = (\n    <>\n      <CitationFavicon url={citation.url} />\n      <span className=\"flex min-w-0 flex-1 flex-wrap items-baseline gap-x-2 gap-y-0.5\">\n        <span className=\"truncate text-sm font-medium text-foreground/80 transition-colors group-hover/citation:text-foreground\">\n          {citation.title}\n        </span>\n        {citation.domain ? (\n          <span className=\"min-w-0 truncate text-xs text-muted-foreground/60\">\n            {citation.domain}\n          </span>\n        ) : null}\n      </span>\n      <span className=\"flex shrink-0 items-center gap-1.5\">\n        <span className=\"grid size-5 place-items-center rounded-md bg-foreground/[0.05] text-[10px] font-semibold tabular-nums text-muted-foreground\">\n          {index}\n        </span>\n        {citation.url ? (\n          <ExternalLink className=\"size-3.5 text-muted-foreground/40 transition-colors group-hover/citation:text-muted-foreground\" />\n        ) : null}\n      </span>\n    </>\n  );\n  const className =\n    \"group/citation flex items-center gap-2 rounded-md px-1.5 py-1 outline-none focus-visible:ring-2 focus-visible:ring-ring\";\n  const id = citationTargetId(idPrefix, citation.id);\n\n  return citation.url ? (\n    <a\n      id={id}\n      href={citation.url}\n      target=\"_blank\"\n      rel=\"noreferrer noopener\"\n      className={className}\n    >\n      {content}\n    </a>\n  ) : (\n    <div id={id} className={className}>\n      {content}\n    </div>\n  );\n}\n\nexport function CitationList({\n  citations,\n  idPrefix,\n  className,\n}: CitationListProps) {\n  const reduce = useReducedMotion() ?? false;\n  const baseId = useId();\n  const resolvedPrefix =\n    idPrefix ?? `citation-list-${baseId.replace(/:/g, \"\")}`;\n\n  return (\n    <div className={cn(\"grid gap-0.5\", className)}>\n      <AnimatePresence mode=\"popLayout\">\n        {citations.map((citation, index) => (\n          <motion.div\n            layout=\"position\"\n            key={citation.id}\n            initial={reduce ? { opacity: 1 } : { opacity: 0, y: 6 }}\n            animate={{ opacity: 1, y: 0 }}\n            exit={reduce ? { opacity: 0 } : { opacity: 0, y: -3 }}\n            transition={\n              reduce\n                ? { duration: 0 }\n                : {\n                    opacity: { duration: 0.18, ease: EASE_OUT },\n                    y: SPRING_LAYOUT,\n                    layout: SPRING_LAYOUT,\n                  }\n            }\n          >\n            <CitationRow\n              citation={citation}\n              index={index + 1}\n              idPrefix={resolvedPrefix}\n            />\n          </motion.div>\n        ))}\n      </AnimatePresence>\n    </div>\n  );\n}\n\nexport function Citations({\n  citations,\n  title = \"Sources\",\n  open,\n  defaultOpen = false,\n  onOpenChange,\n  idPrefix,\n  className,\n}: CitationsProps) {\n  const reduce = useReducedMotion() ?? false;\n  const baseId = useId();\n  const contentId = `${baseId}-content`;\n  const resolvedPrefix =\n    idPrefix ?? `citation-${baseId.replace(/:/g, \"\")}`;\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const currentOpen = open ?? internalOpen;\n  const setOpen = useCallback(\n    (next: boolean) => {\n      if (open === undefined) setInternalOpen(next);\n      onOpenChange?.(next);\n    },\n    [onOpenChange, open],\n  );\n\n  return (\n    <div className={cn(\"w-full text-sm\", className)}>\n      <button\n        type=\"button\"\n        aria-expanded={currentOpen}\n        aria-controls={contentId}\n        onClick={() => setOpen(!currentOpen)}\n        className=\"group -ml-1 flex min-h-8 items-center gap-2 rounded-lg px-1 text-left text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n      >\n        <BookOpenText className=\"size-4\" />\n        <span className=\"font-medium\">{title}</span>\n        <span className=\"rounded-full bg-muted px-1.5 py-0.5 text-[10px] font-semibold tabular-nums\">\n          {citations.length}\n        </span>\n        <motion.span\n          aria-hidden=\"true\"\n          animate={{ rotate: currentOpen ? 180 : 0 }}\n          transition={reduce ? { duration: 0 } : SPRING_SWAP}\n          className=\"text-muted-foreground/60\"\n        >\n          <ChevronDown className=\"size-3.5\" />\n        </motion.span>\n      </button>\n\n      <AgentDisclosure\n        id={contentId}\n        open={currentOpen}\n      >\n        <CitationList\n          citations={citations}\n          idPrefix={resolvedPrefix}\n          className=\"mt-1\"\n        />\n      </AgentDisclosure>\n    </div>\n  );\n}\n"},{"path":"lib/ease.ts","type":"registry:lib","target":"@lib/ease.ts","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n\n/** Dragged handles and fills (sliders) — critically damped `useSpring` config,\n * so the value follows the pointer butterily and never rebounds off an end. */\nexport const SPRING_GLIDE = {\n  stiffness: 700,\n  damping: 50,\n  mass: 0.5,\n} as const;\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"},{"path":"lib/favicon.ts","type":"registry:lib","target":"@lib/favicon.ts","content":"/** Resolve a website URL to its conventional root favicon location. */\nexport function getFaviconUrl(value: string) {\n  try {\n    return new URL(\"/favicon.ico\", value).toString();\n  } catch {\n    return null;\n  }\n}\n"}]}