{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"agent-thread","type":"registry:block","title":"Agent Thread","description":"AI agent conversation stream: user message pills, turn headers, markdown typography, thinking & tool-call status rows, approval card, streaming caret, file & diff artifact cards with expand animation, command rows, and hover-revealed turn actions.","author":"UI Lab","dependencies":["clsx","lucide-react","motion","tailwind-merge"],"registryDependencies":[],"cssVars":{"light":{"wb-hover":"rgb(0 0 0 / 0.05)","wb-hover-subtle":"rgb(0 0 0 / 0.04)","wb-hover-strong":"rgb(0 0 0 / 0.1)","wb-hover-stronger":"rgb(0 0 0 / 0.1)","wb-inset":"rgb(0 0 0 / 0.05)","wb-inset-strong":"rgb(0 0 0 / 0.06)","wb-inset-subtle":"rgb(0 0 0 / 0.03)","wb-inset-faint":"rgb(0 0 0 / 0.02)","wb-code-inline":"rgb(0 0 0 / 0.07)","wb-code-block":"rgb(0 0 0 / 0.04)","wb-border":"rgb(0 0 0 / 0.12)","wb-border-subtle":"rgb(0 0 0 / 0.1)","wb-border-strong":"rgb(0 0 0 / 0.17)","wb-border-emphasis":"rgb(0 0 0 / 0.22)","wb-divider":"rgb(0 0 0 / 0.07)","wb-hairline":"rgb(0 0 0 / 0.12)","wb-hairline-soft":"rgb(0 0 0 / 0.1)","wb-hairline-subtle":"rgb(0 0 0 / 0.08)","wb-control-hairline":"rgb(0 0 0 / 0.08)","wb-control-tick":"rgb(0 0 0 / 0.2)","wb-resize-handle":"rgb(0 0 0 / 0.2)","wb-surface":"rgb(255 255 255)","wb-surface-translucent":"oklch(94.5% 0.002 250 / 0.75)","wb-overlay-scrim":"rgb(0 0 0 / 0.06)","wb-surface-raised":"rgb(255 255 255 / 0.95)","wb-surface-composer":"rgb(255 255 255 / 0.9)","wb-card":"rgb(0 0 0 / 0.04)","wb-inverse":"oklch(20.5% 0 0)","wb-inverse-fg":"rgb(255 255 255)","wb-accent-fg":"rgb(255 255 255)","wb-solid-control-fg":"oklch(20.5% 0 0)","wb-shimmer-dim":"rgb(0 0 0 / 0.4)","wb-shimmer-bright":"rgb(0 0 0 / 0.85)","wb-accent":"#339cff","wb-success":"oklch(59.6% 0.145 163.225)","wb-success-border":"oklch(69.6% 0.17 162.48)","wb-danger":"oklch(63.7% 0.237 25.331)","wb-danger-strong":"#fa423e","wb-danger-surface":"oklch(63.7% 0.237 25.331)","wb-warning":"#e25507","wb-warning-text":"oklch(64.6% 0.222 41.116)","wb-warning-hover":"oklch(70.5% 0.213 47.604)","wb-warning-surface":"#e25507"},"dark":{"wb-hover":"rgb(255 255 255 / 0.1)","wb-hover-subtle":"rgb(255 255 255 / 0.05)","wb-hover-strong":"rgb(255 255 255 / 0.1)","wb-hover-stronger":"rgb(255 255 255 / 0.15)","wb-inset":"rgb(255 255 255 / 0.05)","wb-inset-strong":"rgb(255 255 255 / 0.1)","wb-inset-subtle":"rgb(255 255 255 / 0.05)","wb-inset-faint":"rgb(255 255 255 / 0.03)","wb-code-inline":"rgb(255 255 255 / 0.1)","wb-code-block":"rgb(255 255 255 / 0.05)","wb-border":"rgb(255 255 255 / 0.1)","wb-border-subtle":"rgb(255 255 255 / 0.1)","wb-border-strong":"rgb(255 255 255 / 0.15)","wb-border-emphasis":"rgb(255 255 255 / 0.2)","wb-divider":"rgb(255 255 255 / 0.06)","wb-hairline":"rgb(255 255 255 / 0.157)","wb-hairline-soft":"rgb(255 255 255 / 0.15)","wb-hairline-subtle":"rgb(255 255 255 / 0.1)","wb-control-hairline":"rgb(255 255 255 / 0.08)","wb-control-tick":"rgb(255 255 255 / 0.3)","wb-resize-handle":"rgb(255 255 255 / 0.25)","wb-surface":"oklch(20.5% 0 0)","wb-surface-translucent":"oklch(20.5% 0 0 / 0.6)","wb-overlay-scrim":"rgb(0 0 0 / 0.24)","wb-surface-raised":"oklch(26.9% 0 0 / 0.95)","wb-surface-composer":"oklch(26.9% 0 0 / 0.9)","wb-card":"oklch(26.9% 0 0 / 0.5)","wb-inverse":"rgb(255 255 255)","wb-inverse-fg":"oklch(20.5% 0 0)","wb-accent-fg":"rgb(255 255 255)","wb-solid-control-fg":"oklch(20.5% 0 0)","wb-shimmer-dim":"rgb(255 255 255 / 0.4)","wb-shimmer-bright":"rgb(255 255 255 / 0.95)","wb-accent":"#339cff","wb-success":"#40c977","wb-success-border":"oklch(69.6% 0.17 162.48)","wb-danger":"#fa423e","wb-danger-strong":"#fa423e","wb-danger-surface":"oklch(63.7% 0.237 25.331)","wb-warning":"#ff8549","wb-warning-text":"#ff8549","wb-warning-hover":"#e25507","wb-warning-surface":"#e25507"}},"files":[{"path":"components/motion/agent-thread/index.tsx","type":"registry:component","target":"@components/motion/agent-thread/index.tsx","content":"\"use client\";\n// ui-lab-ten.vercel.app/components/blocks/agent-thread\n\nimport { ChevronRight } from \"lucide-react\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { type ReactNode, useLayoutEffect, useRef, useState } from \"react\";\nimport { EASE_OUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\nimport { ThreadShimmerText } from \"./cards\";\nimport { ThreadStreamingCaret } from \"./status\";\n\nexport {\n  ThreadCard,\n  ThreadCardButton,\n  ThreadCommandRow,\n  ThreadDiffCard,\n  ThreadDiffRow,\n  ThreadFileCard,\n  ThreadShimmerText,\n} from \"./cards\";\nexport {\n  ThreadApprovalCard,\n  ThreadBranchSwitcher,\n  ThreadCheckpoint,\n  ThreadElicitation,\n  ThreadErrorState,\n  ThreadScrollPill,\n  ThreadStreamingCaret,\n  ThreadSuggestions,\n  ThreadSystemBanner,\n  ThreadTask,\n  ThreadTaskList,\n  ThreadThinking,\n  ThreadToolCall,\n  ThreadUsage,\n} from \"./status\";\n\nexport interface ThreadProps {\n  className?: string;\n  children?: ReactNode;\n}\n\n/**\n * Conversation column — centers the message stream at a fixed reading\n * width. Renders `children` directly; spacing between items (user messages,\n * turns) comes from each item's own margins rather than a gap here, so a\n * lone item still looks right regardless of what precedes it.\n */\nexport function Thread({ className, children }: ThreadProps) {\n  return (\n    <div className={cn(\"relative mx-auto flex w-full max-w-3xl flex-col px-4\", className)}>\n      {children}\n    </div>\n  );\n}\n\nexport interface ThreadItemProps {\n  className?: string;\n  children?: ReactNode;\n}\n\n/**\n * Entrance wrapper for a stream item (a user message or an agent turn) as it\n * streams in — a short opacity + upward slide, reduced to an opacity-only\n * fade under `useReducedMotion()`. Purely presentational: callers decide\n * whether to wrap a given item at all (e.g. history rendered on first paint\n * may skip it to avoid replaying the entrance).\n */\nexport function ThreadItem({ className, children }: ThreadItemProps) {\n  const reduce = useReducedMotion() ?? false;\n\n  return (\n    <motion.div\n      initial={reduce ? { opacity: 0 } : { opacity: 0, y: 8 }}\n      animate={{ opacity: 1, y: 0 }}\n      transition={{ duration: reduce ? 0.15 : 0.25, ease: EASE_OUT }}\n      className={className}\n    >\n      {children}\n    </motion.div>\n  );\n}\n\nexport interface ThreadUserMessageProps {\n  className?: string;\n  children?: ReactNode;\n}\n\n/** Right-aligned user message — a rounded gray pill capped at 77% of the column width. */\nexport function ThreadUserMessage({ className, children }: ThreadUserMessageProps) {\n  return (\n    <div className={cn(\"group flex w-full flex-col items-end gap-1 py-3\", className)}>\n      <div className=\"max-w-[77%] overflow-hidden break-words rounded-2xl bg-[var(--wb-inset)] px-3 py-2 text-sm leading-[22px]\">\n        {children}\n      </div>\n    </div>\n  );\n}\n\nexport interface ThreadTurnHeaderProps {\n  /** Controlled open state — purely a chevron-rotation signal (see below). */\n  open?: boolean;\n  onOpenChange?: (next: boolean) => void;\n  /** While the turn is still executing: shimmers the label. The header stays fully interactive — a running turn's live work log can be expanded and collapsed too. */\n  working?: boolean;\n  className?: string;\n  children?: ReactNode;\n}\n\n/**\n * Turn-header button, e.g. \"Worked for 2m 4s ›\". Renders only the header\n * itself — label plus a chevron that rotates 90° to signal open/closed —\n * and does not fold or animate any content region below it: pair it with\n * `ThreadCollapse` (wired to the same `open`) to collapse the turn's work\n * log, or render your own region. Works controlled (`open`/`onOpenChange`)\n * or uncontrolled. `working` (the turn is still executing, e.g. a live\n * elapsed-seconds label) only swaps the label into `ThreadShimmerText`;\n * chevron and toggling stay live, since a running turn's work log can be\n * inspected mid-flight.\n */\nexport function ThreadTurnHeader({\n  open: openProp,\n  onOpenChange,\n  working = false,\n  className,\n  children,\n}: ThreadTurnHeaderProps) {\n  const reduce = useReducedMotion() ?? false;\n  const [openState, setOpenState] = useState(false);\n  const open = openProp ?? openState;\n\n  const toggle = () => {\n    const next = !open;\n    setOpenState(next);\n    onOpenChange?.(next);\n  };\n\n  return (\n    <button\n      type=\"button\"\n      aria-expanded={open}\n      onClick={toggle}\n      className={cn(\n        \"-mx-1 my-3 inline-flex items-center gap-1 self-start rounded-lg px-1 text-sm text-muted-foreground transition-colors hover:bg-[var(--wb-hover)]\",\n        className,\n      )}\n    >\n      {working ? <ThreadShimmerText>{children}</ThreadShimmerText> : children}\n      <motion.span\n        aria-hidden\n        className=\"flex text-muted-foreground/60\"\n        animate={reduce ? undefined : { rotate: open ? 90 : 0 }}\n        style={reduce ? { transform: open ? \"rotate(90deg)\" : \"rotate(0deg)\" } : undefined}\n        transition={reduce ? undefined : { duration: 0.2, ease: EASE_OUT }}\n      >\n        <ChevronRight className=\"h-3.5 w-3.5\" />\n      </motion.span>\n    </button>\n  );\n}\n\nexport interface ThreadCollapseProps {\n  open: boolean;\n  className?: string;\n  children?: ReactNode;\n}\n\n/**\n * Measured-height collapse region — the pairing for `ThreadTurnHeader`: put\n * the turn's work log (thinking row, tool calls, interim notes) inside and\n * wire `open` to the header's state so clicking the header collapses and\n * expands it. Height comes from a `ResizeObserver`, so content that changes\n * while open (e.g. rows streaming in) is tracked; animated 0 ↔ measured\n * with `EASE_OUT` (0.25s), switched instantly under `useReducedMotion()`.\n */\nexport function ThreadCollapse({ open, className, children }: ThreadCollapseProps) {\n  const reduce = useReducedMotion() ?? false;\n  const contentRef = useRef<HTMLDivElement>(null);\n  const [contentHeight, setContentHeight] = useState(0);\n\n  useLayoutEffect(() => {\n    const node = contentRef.current;\n    if (!node) return;\n    const update = () => setContentHeight(node.offsetHeight);\n    update();\n    const observer = new ResizeObserver(update);\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, []);\n\n  return (\n    <motion.div\n      initial={false}\n      animate={reduce ? undefined : { height: open ? contentHeight : 0 }}\n      transition={{ duration: 0.25, ease: EASE_OUT }}\n      className={cn(\"overflow-hidden\", reduce && (open ? \"h-auto\" : \"h-0\"), className)}\n    >\n      <div ref={contentRef} className=\"flex flex-col\">\n        {children}\n      </div>\n    </motion.div>\n  );\n}\n\nexport interface ThreadMessageProps {\n  /** Appends a blinking `ThreadStreamingCaret` after `children` while the response is still streaming in. */\n  streaming?: boolean;\n  className?: string;\n  children?: ReactNode;\n}\n\n/**\n * Typography container for agent-authored prose. A pure styling shell — the\n * Markdown → JSX rendering engine is left to the caller (this component\n * doesn't depend on any particular one); descendant selectors give\n * paragraphs, lists and inline marks consistent spacing regardless of which\n * renderer produced them. `streaming` appends a `ThreadStreamingCaret` at\n * the container tail — after a block-level last paragraph that lands on its\n * own line; to embed the caret inside the last line of text, place\n * `ThreadStreamingCaret` directly in your own JSX instead.\n */\nexport function ThreadMessage({ streaming = false, className, children }: ThreadMessageProps) {\n  return (\n    <div\n      className={cn(\n        \"text-sm leading-[22px] text-foreground\",\n        \"[&_p]:mb-[11px] [&_p:last-child]:mb-0 [&_ul]:mb-[11px] [&_ul]:list-disc [&_ul]:pl-[21px] [&_ol]:mb-[11px] [&_ol]:list-decimal [&_ol]:pl-[21px] [&_li]:pl-0.5 [&_a]:underline [&_a]:underline-offset-2 [&_strong]:font-semibold\",\n        className,\n      )}\n    >\n      {children}\n      {streaming ? <ThreadStreamingCaret /> : null}\n    </div>\n  );\n}\n\nexport interface ThreadInlineCodeProps {\n  className?: string;\n  children?: ReactNode;\n}\n\n/** Inline code chip for use inside `ThreadMessage` prose. */\nexport function ThreadInlineCode({ className, children }: ThreadInlineCodeProps) {\n  return (\n    <span\n      className={cn(\n        \"rounded-[6px] bg-[var(--wb-code-inline)] px-1.5 py-px font-mono text-[0.92em]\",\n        className,\n      )}\n    >\n      {children}\n    </span>\n  );\n}\n\nexport interface ThreadCodeBlockProps {\n  /** Small label in the top-right corner, e.g. a language name like \"bash\". */\n  label?: ReactNode;\n  className?: string;\n  children?: ReactNode;\n}\n\n/** Fenced code block for `ThreadMessage` prose — a `pre > code` structure with an optional corner label. */\nexport function ThreadCodeBlock({ label, className, children }: ThreadCodeBlockProps) {\n  return (\n    <div className={cn(\"relative mb-[11px]\", className)}>\n      {label ? (\n        <span className=\"absolute top-2 right-3 text-muted-foreground text-xs\">{label}</span>\n      ) : null}\n      <pre className=\"overflow-x-auto whitespace-pre rounded-xl bg-[var(--wb-code-block)] p-3 font-mono text-sm leading-[22px]\">\n        <code>{children}</code>\n      </pre>\n    </div>\n  );\n}\n\nexport interface ThreadActionBarProps {\n  /** Rendered after the buttons, e.g. a relative send time. */\n  timestamp?: ReactNode;\n  className?: string;\n  children?: ReactNode;\n}\n\n/**\n * Turn-tail action row — hidden until the turn is hovered or a child gains\n * focus. Pair it with a parent that carries the `group/turn` class (see the\n * preview) so `group-hover/turn:opacity-100` has something to key off; a\n * bare hover on the bar itself would only reveal it once the pointer is\n * already over these 20px-tall icons.\n */\nexport function ThreadActionBar({ timestamp, className, children }: ThreadActionBarProps) {\n  return (\n    <div\n      className={cn(\n        \"flex h-5 items-center gap-0.5 text-muted-foreground opacity-0 transition-opacity focus-within:opacity-100 group-hover/turn:opacity-100\",\n        className,\n      )}\n    >\n      {children}\n      {timestamp ? <span className=\"ml-1.5 text-muted-foreground/80 text-xs\">{timestamp}</span> : null}\n    </div>\n  );\n}\n\nexport interface ThreadActionButtonProps {\n  \"aria-label\": string;\n  onClick?: () => void;\n  children?: ReactNode;\n  className?: string;\n}\n\n/** One icon button inside a `ThreadActionBar` (copy, react, share, ...). */\nexport function ThreadActionButton({\n  \"aria-label\": ariaLabel,\n  onClick,\n  children,\n  className,\n}: ThreadActionButtonProps) {\n  return (\n    <button\n      type=\"button\"\n      aria-label={ariaLabel}\n      onClick={onClick}\n      className={cn(\n        \"flex h-6 w-6 items-center justify-center rounded-md transition-colors hover:bg-[var(--wb-hover)] hover:text-foreground\",\n        className,\n      )}\n    >\n      {children}\n    </button>\n  );\n}\n"},{"path":"components/motion/agent-thread/cards.tsx","type":"registry:component","target":"@components/motion/agent-thread/cards.tsx","content":"\"use client\";\n// ui-lab-ten.vercel.app/components/blocks/agent-thread\n\nimport { ChevronDown } from \"lucide-react\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { type ReactNode, useLayoutEffect, useRef, useState } from \"react\";\nimport { EASE_OUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface ThreadShimmerTextProps {\n  className?: string;\n  children?: ReactNode;\n}\n\n/**\n * Loading-state shimmer text — a dim → bright → dim mask window sweeping\n * across the label, shared by working turn headers, thinking labels and\n * running tool/command rows. The sweep is cadenced — a 1s sweep followed by\n * a 2s rest — rather than a continuous loop, so it reads as a periodic\n * pulse of activity instead of a spinner. Deliberately self-contained (no\n * dependency on the standalone text-shimmer component) so the thread block\n * distributes as one unit. Under `useReducedMotion()` it renders a plain\n * muted span with no sweep.\n */\nexport function ThreadShimmerText({ className, children }: ThreadShimmerTextProps) {\n  const reduce = useReducedMotion() ?? false;\n\n  if (reduce) {\n    return <span className={cn(\"text-muted-foreground\", className)}>{children}</span>;\n  }\n\n  return (\n    <motion.span\n      style={{\n        backgroundImage:\n          \"linear-gradient(90deg, var(--wb-shimmer-dim) 0%, var(--wb-shimmer-dim) 40%, var(--wb-shimmer-bright) 50%, var(--wb-shimmer-dim) 60%, var(--wb-shimmer-dim) 100%)\",\n        backgroundSize: \"200% 100%\",\n      }}\n      animate={{ backgroundPosition: [\"100% 0%\", \"-100% 0%\"] }}\n      transition={{ duration: 1, repeat: Infinity, repeatDelay: 2, ease: \"linear\" }}\n      className={cn(\n        \"bg-clip-text text-transparent\",\n        className,\n      )}\n    >\n      {children}\n    </motion.span>\n  );\n}\n\nexport interface ThreadCardProps {\n  className?: string;\n  children?: ReactNode;\n}\n\n/**\n * Shared card shell for file and diff artifacts — a hairline-ringed surface\n * using the same CSS-variable box-shadow technique as `WorkbenchSummaryCard`\n * / `Composer`, so the right hairline shade is picked per color scheme.\n * `my-1` gives two adjacent cards an 8px gap (4px contributed by each).\n */\nexport function ThreadCard({ className, children }: ThreadCardProps) {\n  return (\n    <div\n      style={{ boxShadow: \"0 0 0 0.5px var(--wb-hairline)\" }}\n      className={cn(\n        \"my-1 flex max-w-full flex-col overflow-hidden rounded-xl bg-[var(--wb-card)]\",\n        className,\n      )}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport interface ThreadCardButtonProps {\n  /** \"outline\" (default) draws a hairline border; \"ghost\" is borderless — for a de-emphasized action beside it (e.g. \"Undo\" next to \"Review\"); \"primary\" is a filled emphasis button (e.g. \"Approve\"). */\n  variant?: \"outline\" | \"ghost\" | \"primary\";\n  onClick?: () => void;\n  \"aria-label\"?: string;\n  children?: ReactNode;\n  className?: string;\n}\n\n/** Small action button used inside file/diff/approval card headers. */\nexport function ThreadCardButton({\n  variant = \"outline\",\n  onClick,\n  \"aria-label\": ariaLabel,\n  children,\n  className,\n}: ThreadCardButtonProps) {\n  return (\n    <button\n      type=\"button\"\n      aria-label={ariaLabel}\n      onClick={onClick}\n      className={cn(\n        \"flex h-7 items-center gap-1 rounded-lg text-sm transition-colors\",\n        variant === \"outline\" &&\n          \"border border-[var(--wb-border)] px-2 hover:bg-[var(--wb-hover)]\",\n        variant === \"ghost\" &&\n          \"px-2 text-muted-foreground hover:bg-[var(--wb-hover)] hover:text-foreground\",\n        variant === \"primary\" &&\n          \"bg-foreground px-3 text-background transition-opacity hover:opacity-85\",\n        className,\n      )}\n    >\n      {children}\n    </button>\n  );\n}\n\nexport interface ThreadFileCardProps {\n  /** 24px icon rendered in a 40px rounded slot, e.g. `<FileText className=\"h-6 w-6\" />`. */\n  icon?: ReactNode;\n  title: ReactNode;\n  subtitle?: ReactNode;\n  /** Trailing slot for the caller's own controls, e.g. an \"Open\" `ThreadCardButton`. */\n  action?: ReactNode;\n  className?: string;\n  /** Optional content appended below the header row, extending the card body. */\n  children?: ReactNode;\n}\n\n/** File-artifact card — icon, title/subtitle, and an optional trailing action. */\nexport function ThreadFileCard({\n  icon,\n  title,\n  subtitle,\n  action,\n  className,\n  children,\n}: ThreadFileCardProps) {\n  return (\n    <ThreadCard className={className}>\n      <div className=\"flex items-center gap-3 px-3 py-2.5\">\n        <span className=\"flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-[var(--wb-inset-strong)]\">\n          {icon}\n        </span>\n        <div className=\"min-w-0 flex-1\">\n          <div className=\"truncate font-medium text-sm\">{title}</div>\n          {subtitle ? <div className=\"text-[13px] text-muted-foreground\">{subtitle}</div> : null}\n        </div>\n        {action ? <div className=\"shrink-0\">{action}</div> : null}\n      </div>\n      {children}\n    </ThreadCard>\n  );\n}\n\nexport interface ThreadDiffRowProps {\n  /** Split at the last `/` into a muted directory prefix and a bright filename. Ignored when `children` is passed. */\n  path?: string;\n  added?: number;\n  removed?: number;\n  className?: string;\n  children?: ReactNode;\n}\n\n/** One changed-file row inside a `ThreadDiffCard`. */\nexport function ThreadDiffRow({ path, added, removed, className, children }: ThreadDiffRowProps) {\n  let content = children;\n  if (content === undefined && path !== undefined) {\n    const lastSlash = path.lastIndexOf(\"/\");\n    const prefix = lastSlash >= 0 ? path.slice(0, lastSlash + 1) : \"\";\n    const name = lastSlash >= 0 ? path.slice(lastSlash + 1) : path;\n    content = (\n      <>\n        <span className=\"text-muted-foreground\">{prefix}</span>\n        <span className=\"text-foreground\">{name}</span>\n      </>\n    );\n  }\n\n  return (\n    <div\n      className={cn(\n        \"flex h-9 items-center justify-between gap-3 border-[var(--wb-divider)] border-t-[0.5px] px-3 text-sm\",\n        className,\n      )}\n    >\n      <div className=\"min-w-0 truncate\">{content}</div>\n      {added !== undefined || removed !== undefined ? (\n        <div className=\"flex shrink-0 gap-1.5 text-[13px]\">\n          {added !== undefined ? (\n            <span className=\"text-[var(--wb-success)]\">+{added}</span>\n          ) : null}\n          {removed !== undefined ? (\n            <span className=\"text-[var(--wb-danger)]\">−{removed}</span>\n          ) : null}\n        </div>\n      ) : null}\n    </div>\n  );\n}\n\nexport interface ThreadDiffCardProps {\n  /** 20px icon rendered in a 40px rounded slot, e.g. `<SquarePen className=\"h-5 w-5\" />`. */\n  icon?: ReactNode;\n  title: ReactNode;\n  added?: number;\n  removed?: number;\n  /** Trailing slot for the caller's own controls, e.g. an \"Undo\" ghost button plus a \"Review\" outline button. */\n  actions?: ReactNode;\n  /** Visible `ThreadDiffRow`s. */\n  children?: ReactNode;\n  /** Extra `ThreadDiffRow`s revealed by the \"show more\" row below `children`. */\n  hiddenRows?: ReactNode;\n  /** Label for the \"show more\" row, e.g. \"Show 2 more files\". */\n  moreLabel?: ReactNode;\n  /** Row count represented by `hiddenRows` — the \"show more\" row only renders when this is greater than 0. */\n  moreCount?: number;\n  className?: string;\n}\n\n/**\n * Diff/change-summary card — a header (icon, title, +added/−removed counts,\n * trailing actions) followed by a row region for `ThreadDiffRow` children.\n * When `moreCount` is positive, a \"show more\" row is appended after\n * `children`; clicking it reveals `hiddenRows` with a measured height 0 →\n * auto tween (`EASE_OUT`, 0.25s) and hides the row itself.\n * `useReducedMotion()` swaps the tween for an instant show, matching the\n * measure-with-`ResizeObserver` idiom used by `BouncyAccordion`.\n */\nexport function ThreadDiffCard({\n  icon,\n  title,\n  added,\n  removed,\n  actions,\n  children,\n  hiddenRows,\n  moreLabel,\n  moreCount = 0,\n  className,\n}: ThreadDiffCardProps) {\n  const reduce = useReducedMotion() ?? false;\n  const [expanded, setExpanded] = useState(false);\n  const contentRef = useRef<HTMLDivElement>(null);\n  const [contentHeight, setContentHeight] = useState(0);\n\n  useLayoutEffect(() => {\n    const node = contentRef.current;\n    if (!node) return;\n    const update = () => setContentHeight(node.offsetHeight);\n    update();\n    const observer = new ResizeObserver(update);\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, []);\n\n  const showMoreRow = moreCount > 0 && !expanded;\n\n  return (\n    <ThreadCard className={className}>\n      <div className=\"flex items-center gap-3 px-3 py-2.5\">\n        <span className=\"flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-[var(--wb-inset-strong)]\">\n          {icon}\n        </span>\n        <div className=\"min-w-0 flex-1\">\n          <div className=\"font-medium text-sm\">{title}</div>\n          {added !== undefined || removed !== undefined ? (\n            <div className=\"flex gap-1.5 text-[13px]\">\n              {added !== undefined ? (\n                <span className=\"text-[var(--wb-success)]\">+{added}</span>\n              ) : null}\n              {removed !== undefined ? (\n                <span className=\"text-[var(--wb-danger)]\">−{removed}</span>\n              ) : null}\n            </div>\n          ) : null}\n        </div>\n        {actions ? <div className=\"flex shrink-0 items-center gap-1\">{actions}</div> : null}\n      </div>\n      {children}\n      {moreCount > 0 ? (\n        <>\n          <motion.div\n            initial={false}\n            animate={reduce ? undefined : { height: expanded ? contentHeight : 0 }}\n            transition={{ duration: 0.25, ease: EASE_OUT }}\n            className={cn(\"overflow-hidden\", reduce && (expanded ? \"h-auto\" : \"h-0\"))}\n          >\n            <div ref={contentRef}>{hiddenRows}</div>\n          </motion.div>\n          {showMoreRow ? (\n            <button\n              type=\"button\"\n              onClick={() => setExpanded(true)}\n              className=\"flex w-full items-center gap-1 px-3 py-2 text-[13px] text-muted-foreground transition-colors hover:bg-[var(--wb-hover)]\"\n            >\n              {moreLabel}\n              <ChevronDown className=\"h-3.5 w-3.5\" />\n            </button>\n          ) : null}\n        </>\n      ) : null}\n    </ThreadCard>\n  );\n}\n\nexport interface ThreadCommandRowProps {\n  /** 16px leading icon, e.g. `<SquareTerminal className=\"h-4 w-4\" />`. */\n  icon?: ReactNode;\n  /** Pulses the icon's opacity while the command is executing. */\n  running?: boolean;\n  className?: string;\n  children?: ReactNode;\n}\n\n/** Single command-execution line — a leading icon (pulsing while `running`, with the text shimmering) followed by the command text. */\nexport function ThreadCommandRow({ icon, running, className, children }: ThreadCommandRowProps) {\n  const reduce = useReducedMotion() ?? false;\n\n  return (\n    <div className={cn(\"flex items-center gap-2 py-1 text-sm text-muted-foreground\", className)}>\n      {icon ? (\n        running ? (\n          <motion.span\n            className=\"flex h-4 w-4 shrink-0 items-center justify-center\"\n            animate={reduce ? { opacity: 1 } : { opacity: [0.4, 1, 0.4] }}\n            transition={reduce ? undefined : { duration: 1.2, repeat: Infinity, ease: \"easeInOut\" }}\n          >\n            {icon}\n          </motion.span>\n        ) : (\n          <span className=\"flex h-4 w-4 shrink-0 items-center justify-center\">{icon}</span>\n        )\n      ) : null}\n      {running ? <ThreadShimmerText>{children}</ThreadShimmerText> : children}\n    </div>\n  );\n}\n"},{"path":"components/motion/agent-thread/status.tsx","type":"registry:component","target":"@components/motion/agent-thread/status.tsx","content":"\"use client\";\n// ui-lab-ten.vercel.app/components/blocks/agent-thread\n\nimport { ArrowDown, Check, ChevronLeft, ChevronRight, CircleAlert, History } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { Fragment, type ReactNode, useLayoutEffect, useRef, useState } from \"react\";\nimport { EASE_OUT, SPRING_PANEL } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\nimport { ThreadCard, ThreadCardButton, ThreadShimmerText } from \"./cards\";\n\nexport type ThreadToolCallStatus = \"running\" | \"done\" | \"error\" | \"stopped\";\n\nexport interface ThreadToolCallProps {\n  /** 16px leading icon, e.g. `<Globe className=\"h-4 w-4\" />`. */\n  icon?: ReactNode;\n  status?: ThreadToolCallStatus;\n  /** Supplement after the label — e.g. a mono query or path; wrapping (such as `font-mono text-[13px]`) is the caller's choice. */\n  detail?: ReactNode;\n  /** Trailing elapsed-time readout. */\n  elapsed?: ReactNode;\n  className?: string;\n  /** The label, verb-tensed by the caller: \"Searching the web\" while running, \"Searched the web\" when done. */\n  children?: ReactNode;\n}\n\n/**\n * Generic tool-invocation row — web searches, file reads, directory\n * listings, MCP tools and anything else the agent runs mid-turn. The\n * general-purpose sibling of `ThreadCommandRow`, which stays around as the\n * command-scenario interface; both share the same row layout. `running`\n * pulses the icon and shimmers the label (`detail` stays static); `done` is\n * fully static and muted; `error` paints icon and label red while `detail`\n * stays muted; `stopped` stays muted with the caller wording the label\n * (e.g. \"Stopped command\").\n */\nexport function ThreadToolCall({\n  icon,\n  status = \"done\",\n  detail,\n  elapsed,\n  className,\n  children,\n}: ThreadToolCallProps) {\n  const reduce = useReducedMotion() ?? false;\n  const running = status === \"running\";\n  const error = status === \"error\";\n\n  return (\n    <div className={cn(\"flex items-center gap-2 py-1 text-sm text-muted-foreground\", className)}>\n      {icon ? (\n        running ? (\n          <motion.span\n            className=\"flex h-4 w-4 shrink-0 items-center justify-center\"\n            animate={reduce ? { opacity: 1 } : { opacity: [0.4, 1, 0.4] }}\n            transition={reduce ? undefined : { duration: 1.2, repeat: Infinity, ease: \"easeInOut\" }}\n          >\n            {icon}\n          </motion.span>\n        ) : (\n          <span\n            className={cn(\n              \"flex h-4 w-4 shrink-0 items-center justify-center\",\n              error && \"text-[var(--wb-danger)]\",\n            )}\n          >\n            {icon}\n          </span>\n        )\n      ) : null}\n      {running ? (\n        <ThreadShimmerText>{children}</ThreadShimmerText>\n      ) : (\n        <span className={cn(error && \"text-[var(--wb-danger)]\")}>{children}</span>\n      )}\n      {detail !== undefined && detail !== null ? (\n        <span className=\"min-w-0 truncate\">{detail}</span>\n      ) : null}\n      {elapsed !== undefined && elapsed !== null ? (\n        <span className=\"text-[13px] text-muted-foreground/70 tabular-nums\">{elapsed}</span>\n      ) : null}\n    </div>\n  );\n}\n\nexport interface ThreadThinkingProps {\n  /** Still reasoning: the label shimmers, the chevron is hidden and expansion is disabled (there is no summary to show yet). */\n  thinking?: boolean;\n  /** Controlled open state for the summary region. */\n  open?: boolean;\n  onOpenChange?: (next: boolean) => void;\n  className?: string;\n  /** \"Thinking…\" while `thinking`, then e.g. \"Thought for 8s\". */\n  label: ReactNode;\n  /** Optional reasoning summary revealed below the header once `thinking` is over. */\n  children?: ReactNode;\n}\n\n/**\n * Reasoning-state row. Visually matches `ThreadTurnHeader` (deliberately an\n * independent implementation to avoid coupling): while `thinking` the label\n * shimmers with no chevron; once done, pass a summary as `children` to get\n * an expandable region — measured with a `ResizeObserver` and animated\n * height 0 ↔ measured (`EASE_OUT`, 0.25s), shown instantly under\n * `useReducedMotion()` — rendered as a left-ruled quote block. Works\n * controlled (`open`/`onOpenChange`) or uncontrolled.\n */\nexport function ThreadThinking({\n  thinking = false,\n  open: openProp,\n  onOpenChange,\n  className,\n  label,\n  children,\n}: ThreadThinkingProps) {\n  const reduce = useReducedMotion() ?? false;\n  const [openState, setOpenState] = useState(false);\n  const contentRef = useRef<HTMLDivElement>(null);\n  const [contentHeight, setContentHeight] = useState(0);\n\n  const expandable = !thinking && children !== undefined && children !== null;\n  const open = expandable && (openProp ?? openState);\n\n  const toggle = () => {\n    const next = !open;\n    setOpenState(next);\n    onOpenChange?.(next);\n  };\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: `expandable` is the trigger — the summary region only mounts once thinking ends, so the observer must re-attach to the node the effect reads from the DOM at that point.\n  useLayoutEffect(() => {\n    const node = contentRef.current;\n    if (!node) return;\n    const update = () => setContentHeight(node.offsetHeight);\n    update();\n    const observer = new ResizeObserver(update);\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, [expandable]);\n\n  return (\n    <div className={cn(\"flex flex-col\", className)}>\n      <button\n        type=\"button\"\n        disabled={!expandable}\n        aria-expanded={expandable ? open : undefined}\n        onClick={toggle}\n        className=\"-mx-1 my-1 inline-flex items-center gap-1 self-start rounded-lg px-1 text-sm text-muted-foreground transition-colors hover:bg-[var(--wb-hover)] disabled:pointer-events-none\"\n      >\n        {thinking ? <ThreadShimmerText>{label}</ThreadShimmerText> : label}\n        {expandable ? (\n          <motion.span\n            aria-hidden\n            className=\"flex text-muted-foreground/60\"\n            animate={reduce ? undefined : { rotate: open ? 90 : 0 }}\n            style={reduce ? { transform: open ? \"rotate(90deg)\" : \"rotate(0deg)\" } : undefined}\n            transition={reduce ? undefined : { duration: 0.2, ease: EASE_OUT }}\n          >\n            <ChevronRight className=\"h-3.5 w-3.5\" />\n          </motion.span>\n        ) : null}\n      </button>\n      {expandable ? (\n        <motion.div\n          initial={false}\n          animate={reduce ? undefined : { height: open ? contentHeight : 0 }}\n          transition={{ duration: 0.25, ease: EASE_OUT }}\n          className={cn(\"overflow-hidden\", reduce && (open ? \"h-auto\" : \"h-0\"))}\n        >\n          <div\n            ref={contentRef}\n            className=\"border-[var(--wb-border)] border-l-2 py-1 pl-3 text-[13px] text-muted-foreground leading-[20px]\"\n          >\n            {children}\n          </div>\n        </motion.div>\n      ) : null}\n    </div>\n  );\n}\n\nexport interface ThreadStreamingCaretProps {\n  className?: string;\n}\n\n/**\n * Blinking block caret appended to text that is still streaming in. Inline —\n * drop it right after the last streamed character. Static at half opacity\n * under `useReducedMotion()`.\n */\nexport function ThreadStreamingCaret({ className }: ThreadStreamingCaretProps) {\n  const reduce = useReducedMotion() ?? false;\n  const base = \"ml-0.5 inline-block h-3.5 w-[7px] translate-y-[2px] rounded-[2px] bg-foreground/70\";\n\n  if (reduce) {\n    return <span aria-hidden className={cn(base, \"opacity-50\", className)} />;\n  }\n\n  return (\n    <motion.span\n      aria-hidden\n      className={cn(base, className)}\n      animate={{ opacity: [1, 0.15, 1] }}\n      transition={{ duration: 1, repeat: Infinity, ease: \"easeInOut\" }}\n    />\n  );\n}\n\nexport type ThreadApprovalStatus = \"pending\" | \"approved\" | \"denied\";\n\nexport interface ThreadApprovalCardProps {\n  /** 20px icon rendered in a 40px rounded slot, e.g. `<ShieldAlert className=\"h-5 w-5\" />`. */\n  icon?: ReactNode;\n  title: ReactNode;\n  /** Sub-line under the title — wraps, never truncated. */\n  description?: ReactNode;\n  /** Optional mono one-liner of what will run. */\n  command?: ReactNode;\n  status?: ThreadApprovalStatus;\n  /** Shown in place of the buttons once `status` is no longer \"pending\". */\n  resolution?: ReactNode;\n  onApprove?: () => void;\n  onDeny?: () => void;\n  approveLabel?: ReactNode;\n  denyLabel?: ReactNode;\n  className?: string;\n}\n\n/**\n * Approval-gate card — the agent pauses and asks before running something\n * sensitive. While `pending`, a ghost Deny and a primary Approve button sit\n * at the trailing edge; once resolved, `resolution` replaces them (green\n * when `approved`, muted when `denied`). Built on `ThreadCard`, so it shares\n * the hairline surface with the file and diff cards.\n */\nexport function ThreadApprovalCard({\n  icon,\n  title,\n  description,\n  command,\n  status = \"pending\",\n  resolution,\n  onApprove,\n  onDeny,\n  approveLabel = \"Approve\",\n  denyLabel = \"Deny\",\n  className,\n}: ThreadApprovalCardProps) {\n  return (\n    <ThreadCard className={className}>\n      <div className=\"flex items-center gap-3 px-3 py-2.5\">\n        <span className=\"flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-[var(--wb-inset-strong)]\">\n          {icon}\n        </span>\n        <div className=\"min-w-0 flex-1\">\n          <div className=\"font-medium text-sm\">{title}</div>\n          {description ? <div className=\"text-[13px] text-muted-foreground\">{description}</div> : null}\n        </div>\n        <div className=\"flex shrink-0 items-center gap-1\">\n          {status === \"pending\" ? (\n            <>\n              <ThreadCardButton variant=\"ghost\" onClick={onDeny}>\n                {denyLabel}\n              </ThreadCardButton>\n              <ThreadCardButton variant=\"primary\" onClick={onApprove}>\n                {approveLabel}\n              </ThreadCardButton>\n            </>\n          ) : resolution ? (\n            <span\n              className={cn(\n                \"text-[13px]\",\n                status === \"approved\"\n                  ? \"text-[var(--wb-success)]\"\n                  : \"text-muted-foreground\",\n              )}\n            >\n              {resolution}\n            </span>\n          ) : null}\n        </div>\n      </div>\n      {command ? (\n        <div className=\"mx-3 mb-3 rounded-lg bg-[var(--wb-code-block)] px-3 py-2 font-mono text-[13px]\">\n          {command}\n        </div>\n      ) : null}\n    </ThreadCard>\n  );\n}\n\nexport interface ThreadElicitationProps {\n  /** 20px icon rendered in a 40px rounded slot, e.g. `<MessageCircleQuestion className=\"h-5 w-5\" />`. */\n  icon?: ReactNode;\n  prompt: ReactNode;\n  options: { value: string; label: ReactNode; description?: ReactNode }[];\n  /** Selected option value; `null`/`undefined` means still awaiting an answer. */\n  value?: string | null;\n  onSelect?: (value: string) => void;\n  className?: string;\n}\n\n/**\n * Blocking clarification picker — the agent pauses on an ambiguous request\n * and offers a fixed set of answers instead of free text. Built on\n * `ThreadCard`, matching `ThreadApprovalCard`'s header row (icon slot plus a\n * medium-weight prompt). Once `value` is set the whole list locks: the\n * matching option gets a primary ring and a trailing check, the rest dim —\n * the same pending → resolved shape as `ThreadApprovalCard`, but for an\n * N-way choice instead of approve/deny.\n */\nexport function ThreadElicitation({\n  icon,\n  prompt,\n  options,\n  value = null,\n  onSelect,\n  className,\n}: ThreadElicitationProps) {\n  return (\n    <ThreadCard className={className}>\n      <div className=\"flex items-center gap-3 px-3 py-2.5\">\n        <span className=\"flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-[var(--wb-inset-strong)]\">\n          {icon}\n        </span>\n        <div className=\"min-w-0 flex-1 font-medium text-sm\">{prompt}</div>\n      </div>\n      <div className=\"flex flex-col gap-1 px-3 pb-3\">\n        {options.map((option) => {\n          const selected = value === option.value;\n          const locked = value !== null;\n          return (\n            <button\n              key={option.value}\n              type=\"button\"\n              disabled={locked}\n              onClick={() => onSelect?.(option.value)}\n              className={cn(\n                \"flex w-full items-start gap-2 rounded-lg border border-[var(--wb-border)] px-3 py-2 text-left text-sm transition-colors\",\n                \"hover:bg-[var(--wb-hover-subtle)] disabled:pointer-events-none\",\n                selected && \"border-[var(--wb-accent)] ring-1 ring-[var(--wb-accent)]/30\",\n                locked && !selected && \"opacity-50\",\n              )}\n            >\n              <span className=\"min-w-0 flex-1\">\n                <span className=\"block\">{option.label}</span>\n                {option.description ? (\n                  <span className=\"block text-[13px] text-muted-foreground\">\n                    {option.description}\n                  </span>\n                ) : null}\n              </span>\n              {selected ? (\n                <Check className=\"mt-0.5 h-3.5 w-3.5 shrink-0 text-[var(--wb-accent)]\" />\n              ) : null}\n            </button>\n          );\n        })}\n      </div>\n    </ThreadCard>\n  );\n}\n\nexport interface ThreadErrorStateProps {\n  message: ReactNode;\n  detail?: ReactNode;\n  onRetry?: () => void;\n  retryLabel?: ReactNode;\n  className?: string;\n}\n\n/**\n * Inline error row — a failed tool call, request or turn surfaced as a\n * red-tinted banner with a retry action. Not built on `ThreadCard`: the\n * failure isn't a browsable artifact, just a transient state to recover\n * from. `onRetry` renders a `ThreadCardButton`, so retrying matches every\n * other card's action styling.\n */\nexport function ThreadErrorState({\n  message,\n  detail,\n  onRetry,\n  retryLabel = \"Retry\",\n  className,\n}: ThreadErrorStateProps) {\n  return (\n    <div\n      className={cn(\n        \"flex items-start gap-2.5 rounded-xl border border-[var(--wb-danger-surface)]/25 bg-[var(--wb-danger-surface)]/[0.06] px-3 py-2.5 text-sm\",\n        className,\n      )}\n    >\n      <CircleAlert className=\"mt-0.5 h-4 w-4 shrink-0 text-[var(--wb-danger)]\" />\n      <div className=\"min-w-0 flex-1\">\n        <div className=\"text-foreground\">{message}</div>\n        {detail ? <div className=\"text-[13px] text-muted-foreground\">{detail}</div> : null}\n      </div>\n      {onRetry ? (\n        <ThreadCardButton onClick={onRetry} className=\"shrink-0\">\n          {retryLabel}\n        </ThreadCardButton>\n      ) : null}\n    </div>\n  );\n}\n\nexport interface ThreadSystemBannerProps {\n  icon?: ReactNode;\n  children?: ReactNode;\n  className?: string;\n}\n\n/** Centered pill for low-emphasis system notices — e.g. \"Model switched to\n * 5.6\" — dropped inline in the stream without a `ThreadItem` entrance\n * wrapper (it's a passive notice, not a message). */\nexport function ThreadSystemBanner({ icon, children, className }: ThreadSystemBannerProps) {\n  return (\n    <div\n      className={cn(\n        \"mx-auto my-2 flex w-fit items-center gap-1.5 rounded-full bg-[var(--wb-inset)] px-3 py-1 text-muted-foreground text-xs\",\n        className,\n      )}\n    >\n      {icon ? (\n        <span className=\"flex h-3 w-3 shrink-0 items-center justify-center\">{icon}</span>\n      ) : null}\n      {children}\n    </div>\n  );\n}\n\nexport interface ThreadBranchSwitcherProps {\n  /** 1-based position of the branch currently shown. */\n  index: number;\n  count: number;\n  onPrev?: () => void;\n  onNext?: () => void;\n  className?: string;\n}\n\n/**\n * Prev/next control for switching between sibling response branches (e.g.\n * regenerated replies) — small chevron buttons flanking a tabular-nums\n * \"2/3\" readout, disabled past either end. The readout crossfades with a\n * short y-shift on change (`AnimatePresence mode=\"popLayout\"`), reduced to\n * an instant swap under `useReducedMotion()`.\n */\nexport function ThreadBranchSwitcher({\n  index,\n  count,\n  onPrev,\n  onNext,\n  className,\n}: ThreadBranchSwitcherProps) {\n  const reduce = useReducedMotion() ?? false;\n\n  return (\n    <div className={cn(\"flex items-center gap-0.5 text-muted-foreground text-xs\", className)}>\n      <button\n        type=\"button\"\n        aria-label=\"Previous branch\"\n        disabled={index <= 1}\n        onClick={onPrev}\n        className=\"flex h-5 w-5 items-center justify-center rounded hover:bg-[var(--wb-hover)] disabled:pointer-events-none disabled:opacity-30\"\n      >\n        <ChevronLeft className=\"h-3 w-3\" />\n      </button>\n      <span className=\"relative inline-flex h-4 min-w-[2.5ch] items-center justify-center overflow-hidden tabular-nums\">\n        <AnimatePresence mode=\"popLayout\" initial={false}>\n          <motion.span\n            key={`${index}/${count}`}\n            initial={reduce ? { opacity: 0 } : { opacity: 0, y: 4 }}\n            animate={{ opacity: 1, y: 0 }}\n            exit={reduce ? { opacity: 0 } : { opacity: 0, y: -4 }}\n            transition={{ duration: 0.15, ease: EASE_OUT }}\n            className=\"absolute inset-0 flex items-center justify-center\"\n          >\n            {index}/{count}\n          </motion.span>\n        </AnimatePresence>\n      </span>\n      <button\n        type=\"button\"\n        aria-label=\"Next branch\"\n        disabled={index >= count}\n        onClick={onNext}\n        className=\"flex h-5 w-5 items-center justify-center rounded hover:bg-[var(--wb-hover)] disabled:pointer-events-none disabled:opacity-30\"\n      >\n        <ChevronRight className=\"h-3 w-3\" />\n      </button>\n    </div>\n  );\n}\n\nexport interface ThreadSuggestionsProps {\n  suggestions: { value: string; label: ReactNode }[];\n  onSelect?: (value: string) => void;\n  className?: string;\n}\n\n/**\n * Row of tappable follow-up prompts offered after an agent turn. Each chip\n * stagger-fades in (opacity plus a 4px rise, staggered 0.05s per index) so\n * the row reads as offered rather than dumped in all at once, reduced to an\n * instant render under `useReducedMotion()`.\n */\nexport function ThreadSuggestions({ suggestions, onSelect, className }: ThreadSuggestionsProps) {\n  const reduce = useReducedMotion() ?? false;\n\n  return (\n    <div className={cn(\"flex flex-wrap gap-1.5 py-2\", className)}>\n      {suggestions.map((suggestion, i) => (\n        <motion.span\n          key={suggestion.value}\n          initial={reduce ? { opacity: 0 } : { opacity: 0, y: 4 }}\n          animate={{ opacity: 1, y: 0 }}\n          transition={{ duration: 0.2, ease: EASE_OUT, delay: reduce ? 0 : i * 0.05 }}\n        >\n          <button\n            type=\"button\"\n            onClick={() => onSelect?.(suggestion.value)}\n            className=\"h-7 rounded-full border border-[var(--wb-border)] px-3 text-[13px] text-muted-foreground transition-colors hover:border-[var(--wb-border-emphasis)] hover:text-foreground\"\n          >\n            {suggestion.label}\n          </button>\n        </motion.span>\n      ))}\n    </div>\n  );\n}\n\nexport type ThreadTaskStatus = \"pending\" | \"active\" | \"done\";\n\nexport interface ThreadTaskListProps {\n  title?: ReactNode;\n  /** Short counter, e.g. \"2/5\". Swaps with a `popLayout` crossfade when it changes. */\n  progress?: ReactNode;\n  className?: string;\n  children?: ReactNode;\n}\n\n/**\n * Checklist card for a long-running agent task, built on `ThreadCard` so it\n * shares the hairline ring and tint with the file/diff/approval cards. The\n * optional title row pairs a medium-weight label with a tabular-nums\n * progress readout on the trailing edge; the readout crossfades with a\n * short y-shift on change (`AnimatePresence mode=\"popLayout\"`, mirroring\n * `ThreadBranchSwitcher`'s counter), reduced to an opacity-only swap under\n * `useReducedMotion()`. Compose `ThreadTask` rows as `children`.\n */\nexport function ThreadTaskList({ title, progress, className, children }: ThreadTaskListProps) {\n  const reduce = useReducedMotion() ?? false;\n\n  return (\n    <ThreadCard className={cn(\"px-3 py-2\", className)}>\n      {title !== undefined ? (\n        <div className=\"flex items-center justify-between pb-1\">\n          <span className=\"font-medium text-[13px]\">{title}</span>\n          {progress !== undefined ? (\n            <span className=\"relative inline-flex h-4 min-w-[2.5ch] items-center justify-center overflow-hidden text-muted-foreground text-xs tabular-nums\">\n              <AnimatePresence mode=\"popLayout\" initial={false}>\n                <motion.span\n                  key={String(progress)}\n                  initial={reduce ? { opacity: 0 } : { opacity: 0, y: 4 }}\n                  animate={{ opacity: 1, y: 0 }}\n                  exit={reduce ? { opacity: 0 } : { opacity: 0, y: -4 }}\n                  transition={{ duration: 0.15, ease: EASE_OUT }}\n                  className=\"absolute inset-0 flex items-center justify-center\"\n                >\n                  {progress}\n                </motion.span>\n              </AnimatePresence>\n            </span>\n          ) : null}\n        </div>\n      ) : null}\n      <div className=\"flex flex-col\">{children}</div>\n    </ThreadCard>\n  );\n}\n\nexport interface ThreadTaskProps {\n  status?: ThreadTaskStatus;\n  className?: string;\n  children?: ReactNode;\n}\n\n/**\n * Single row inside a `ThreadTaskList` — a 16px status slot followed by the\n * task's label. `pending` is a hollow ring, `active` is a solid blue dot\n * wrapped in a pulsing ring (frozen, not removed, under\n * `useReducedMotion()`) and its label runs through `ThreadShimmerText`,\n * `done` is a check mark. The icon swap itself is a springy scale pop\n * (`SPRING_PANEL`, `AnimatePresence mode=\"wait\"`), reduced to a plain\n * opacity cut. `done` text stays muted rather than a celebratory color —\n * finishing a step should read as quiet progress, not an event.\n */\nexport function ThreadTask({ status = \"pending\", className, children }: ThreadTaskProps) {\n  const reduce = useReducedMotion() ?? false;\n\n  return (\n    <div className={cn(\"flex items-center gap-2 py-1 text-sm\", className)}>\n      <span className=\"relative flex h-4 w-4 shrink-0 items-center justify-center\">\n        <AnimatePresence mode=\"wait\" initial={false}>\n          <motion.span\n            key={status}\n            initial={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.5 }}\n            animate={{ opacity: 1, scale: 1 }}\n            exit={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.5 }}\n            transition={reduce ? { duration: 0.15, ease: EASE_OUT } : SPRING_PANEL}\n            className=\"flex items-center justify-center\"\n          >\n            {status === \"done\" ? (\n              <Check className=\"h-3.5 w-3.5 text-[var(--wb-success)]\" />\n            ) : status === \"active\" ? (\n              <span className=\"relative flex h-2 w-2 items-center justify-center\">\n                {reduce ? (\n                  <span\n                    aria-hidden\n                    className=\"absolute inset-0 rounded-full bg-[var(--wb-accent)] opacity-25\"\n                  />\n                ) : (\n                  <motion.span\n                    aria-hidden\n                    className=\"absolute inset-0 rounded-full bg-[var(--wb-accent)]\"\n                    animate={{ scale: [1, 1.8], opacity: [0.5, 0] }}\n                    transition={{ duration: 1.6, repeat: Infinity, ease: \"easeOut\" }}\n                  />\n                )}\n                <span className=\"h-2 w-2 rounded-full bg-[var(--wb-accent)]\" />\n              </span>\n            ) : (\n              <span className=\"h-3.5 w-3.5 rounded-full border-[1.5px] border-[var(--wb-border-emphasis)]\" />\n            )}\n          </motion.span>\n        </AnimatePresence>\n      </span>\n      {status === \"active\" ? (\n        <ThreadShimmerText>{children}</ThreadShimmerText>\n      ) : (\n        <span className=\"text-muted-foreground\">{children}</span>\n      )}\n    </div>\n  );\n}\n\nexport interface ThreadScrollPillProps {\n  open: boolean;\n  count?: number;\n  onClick?: () => void;\n  className?: string;\n}\n\n/**\n * \"New messages\" pill for auto-scrolling thread containers. Needs a\n * `relative` ancestor to anchor against — toggle `open` when the user has\n * scrolled away from the bottom while new content streams in below (see the\n * preview). Springs up from the bottom edge (`SPRING_PANEL`), reduced to an\n * opacity-only fade under `useReducedMotion()`.\n */\nexport function ThreadScrollPill({ open, count, onClick, className }: ThreadScrollPillProps) {\n  const reduce = useReducedMotion() ?? false;\n\n  return (\n    <AnimatePresence>\n      {open ? (\n        <motion.button\n          type=\"button\"\n          onClick={onClick}\n          initial={reduce ? { opacity: 0 } : { opacity: 0, y: 8 }}\n          animate={{ opacity: 1, y: 0 }}\n          exit={reduce ? { opacity: 0 } : { opacity: 0, y: 8 }}\n          transition={reduce ? { duration: 0.15, ease: EASE_OUT } : SPRING_PANEL}\n          className={cn(\n            \"absolute bottom-4 left-1/2 z-20 flex -translate-x-1/2 items-center gap-1 rounded-full bg-[var(--wb-inverse)] px-3 py-1.5 text-[var(--wb-inverse-fg)] text-xs shadow-lg\",\n            className,\n          )}\n        >\n          <ArrowDown className=\"h-3 w-3\" />\n          {count !== undefined ? `${count} new messages` : null}\n        </motion.button>\n      ) : null}\n    </AnimatePresence>\n  );\n}\n\nexport interface ThreadCheckpointProps {\n  /** @default \"Checkpoint\" */\n  label?: ReactNode;\n  timestamp?: ReactNode;\n  onRestore?: () => void;\n  /** @default \"Restore\" */\n  restoreLabel?: ReactNode;\n  className?: string;\n}\n\n/**\n * Rollback marker dividing the stream at a point the user can restore to —\n * a hairline rule on either side of a pill carrying a history icon, the\n * checkpoint label and an optional timestamp. When `onRestore` is passed, a\n * trailing text button (\"Restore\") is appended inside the same pill.\n */\nexport function ThreadCheckpoint({\n  label = \"Checkpoint\",\n  timestamp,\n  onRestore,\n  restoreLabel = \"Restore\",\n  className,\n}: ThreadCheckpointProps) {\n  return (\n    <div className={cn(\"relative flex items-center gap-3 py-2\", className)}>\n      <span className=\"h-px flex-1 bg-[var(--wb-divider)]\" />\n      <span className=\"flex h-6 items-center gap-1.5 rounded-full border border-[var(--wb-border)] px-2.5 text-xs text-muted-foreground\">\n        <History className=\"h-3 w-3\" />\n        {label}\n        {timestamp !== undefined && timestamp !== null ? (\n          <span className=\"text-muted-foreground/70\">{timestamp}</span>\n        ) : null}\n        {onRestore ? (\n          <button\n            type=\"button\"\n            onClick={onRestore}\n            className=\"text-xs transition-colors hover:text-foreground\"\n          >\n            {restoreLabel}\n          </button>\n        ) : null}\n      </span>\n      <span className=\"h-px flex-1 bg-[var(--wb-divider)]\" />\n    </div>\n  );\n}\n\n/** <0.01 keeps 4 decimal places, otherwise 3 — trailing zeros are trimmed either way (e.g. 0.003 → \"$0.003\", not \"$0.0030\"). */\nfunction formatUsageCost(cost: number): string {\n  const decimals = cost < 0.01 ? 4 : 3;\n  const trimmed = cost.toFixed(decimals).replace(/0+$/, \"\").replace(/\\.$/, \"\");\n  return `$${trimmed}`;\n}\n\n/** K/M abbreviation for a raw token count; values under 1000 render as-is. */\nfunction formatUsageTokenCount(count: number): string {\n  if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;\n  if (count >= 1000) return `${(count / 1000).toFixed(1)}K`;\n  return `${count}`;\n}\n\nexport interface ThreadUsageProps {\n  cost?: number;\n  inputTokens?: number;\n  outputTokens?: number;\n  duration?: ReactNode;\n  cacheHitRate?: number;\n  className?: string;\n  children?: ReactNode;\n}\n\n/**\n * Per-message usage/cost line — cost, input/output token counts, duration\n * and cache-hit rate, each shown only when its prop is present and joined\n * by a muted middle dot. Meant to live alongside `ThreadActionBar` in the\n * same hover group; the component itself renders unconditionally, so pass\n * e.g. `\"opacity-0 group-hover/turn:opacity-100\"` in `className` if you want\n * it to reveal on turn hover the way the action bar does.\n */\nexport function ThreadUsage({\n  cost,\n  inputTokens,\n  outputTokens,\n  duration,\n  cacheHitRate,\n  className,\n  children,\n}: ThreadUsageProps) {\n  const fragments: { key: string; node: ReactNode }[] = [];\n\n  if (cost !== undefined) {\n    fragments.push({ key: \"cost\", node: formatUsageCost(cost) });\n  }\n\n  if (inputTokens !== undefined || outputTokens !== undefined) {\n    const inStr = inputTokens !== undefined ? `${formatUsageTokenCount(inputTokens)} in` : null;\n    const outStr = outputTokens !== undefined ? `${formatUsageTokenCount(outputTokens)} out` : null;\n    fragments.push({\n      key: \"tokens\",\n      node: inStr && outStr ? `${inStr} / ${outStr}` : (inStr ?? outStr),\n    });\n  }\n\n  if (duration !== undefined && duration !== null) {\n    fragments.push({ key: \"duration\", node: duration });\n  }\n\n  if (cacheHitRate !== undefined) {\n    fragments.push({ key: \"cache\", node: `cache ${Math.round(cacheHitRate * 100)}%` });\n  }\n\n  if (children !== undefined && children !== null) {\n    fragments.push({ key: \"children\", node: children });\n  }\n\n  if (fragments.length === 0) return null;\n\n  return (\n    <div\n      className={cn(\n        \"flex flex-wrap items-center gap-x-1.5 gap-y-0.5 text-xs text-muted-foreground/80 tabular-nums\",\n        className,\n      )}\n    >\n      {fragments.map((fragment, i) => (\n        <Fragment key={fragment.key}>\n          {i > 0 ? <span className=\"text-muted-foreground/40\">·</span> : null}\n          <span>{fragment.node}</span>\n        </Fragment>\n      ))}\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"},{"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"}]}