{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"agent-trace","type":"registry:block","title":"Agent Trace","description":"Activity rail for agent runs: kind-styled step nodes on a hairline rail, live steps with pulse ring and cadenced shimmer, raw-output expand, parallel tool groups, and nested sub-agent mini traces.","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-trace/index.tsx","type":"registry:component","target":"@components/motion/agent-trace/index.tsx","content":"\"use client\";\n// ui-lab-ten.vercel.app/components/blocks/agent-trace\n\nimport { Check, ChevronRight, Clock, RefreshCw, Sparkles, Wrench } from \"lucide-react\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { type ComponentType, type ReactNode, useLayoutEffect, useRef, useState } from \"react\";\nimport { EASE_OUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface TraceProps {\n  className?: string;\n  children?: ReactNode;\n}\n\n/**\n * Activity-rail container for an agent's execution trace — a hairline\n * strung through the center of every step's icon node, top and bottom\n * insets leaving breathing room before the first and after the last step.\n * Purely a positioning shell: `TraceStep`, `TraceGroup` and `TraceSubagent`\n * line their own markers up against it, so the rail visually threads\n * through whatever mix of step kinds is mounted.\n */\nexport function Trace({ className, children }: TraceProps) {\n  return (\n    <div\n      className={cn(\n        \"relative flex flex-col gap-0\",\n        \"before:absolute before:top-3 before:bottom-3 before:left-[11px] before:w-px before:bg-[var(--wb-border)]\",\n        className,\n      )}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport type TraceStepKind = \"plan\" | \"tool\" | \"reflection\" | \"done\";\n\nconst KIND_ICON: Record<TraceStepKind, ComponentType<{ className?: string }>> = {\n  plan: Sparkles,\n  tool: Wrench,\n  reflection: RefreshCw,\n  done: Check,\n};\n\nconst KIND_NODE_STYLE: Record<TraceStepKind, string> = {\n  plan: \"border-[var(--wb-border-strong)] text-muted-foreground\",\n  tool: \"border-[var(--wb-border-strong)] text-muted-foreground\",\n  reflection: \"border-dashed border-[var(--wb-border-strong)] text-muted-foreground\",\n  done: \"border-[var(--wb-success-border)]/40 text-[var(--wb-success)]\",\n};\n\ninterface TraceShimmerTextProps {\n  className?: string;\n  children?: ReactNode;\n}\n\n/**\n * Cadenced shimmer for a step's live label — a 1s sweep followed by a 2s\n * rest, matching the pulse-of-activity read the thread block's shimmer\n * uses. Deliberately reimplemented here rather than imported, so this block\n * distributes as one self-contained unit. Falls back to a plain muted span\n * under `useReducedMotion()`.\n */\nfunction TraceShimmerText({ className, children }: TraceShimmerTextProps) {\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 TraceStepProps {\n  /** Drives the icon-node border style and the default icon; @default \"tool\" */\n  kind?: TraceStepKind;\n  /** 13px icon overriding the `kind` default (Sparkles/Wrench/RefreshCw/Check). */\n  icon?: ReactNode;\n  label: ReactNode;\n  /** Second line under the label. */\n  detail?: ReactNode;\n  /** Right-floating slot, e.g. elapsed time — pass a colored span for a slow-running highlight. */\n  meta?: ReactNode;\n  /** Live state: pulses the icon ring and shimmers the label. */\n  active?: boolean;\n  /** Controlled \"View raw\" open state — only meaningful when `children` is passed. */\n  open?: boolean;\n  onOpenChange?: (next: boolean) => void;\n  /** Raw content revealed by \"View raw\", e.g. a mono JSON block styled by the caller. */\n  children?: ReactNode;\n  className?: string;\n}\n\n/**\n * One row on the activity rail. Renders an icon node on the rail (styled by\n * `kind`), a label (shimmering while `active`), an optional detail line and\n * a right-floating `meta` slot. When `children` is passed, a \"View raw\"\n * button appends to the label row and toggles a height-measured region\n * below — controlled (`open`/`onOpenChange`) or uncontrolled, matching the\n * `ThreadCollapse` idiom (`ResizeObserver` + 0 ↔ measured height tween,\n * instant under `useReducedMotion()`). Mounts with the same opacity + slide\n * entrance as `ThreadItem`; callers drive when a step mounts to script the\n * trace's pacing.\n */\nexport function TraceStep({\n  kind = \"tool\",\n  icon,\n  label,\n  detail,\n  meta,\n  active = false,\n  open: openProp,\n  onOpenChange,\n  children,\n  className,\n}: TraceStepProps) {\n  const reduce = useReducedMotion() ?? false;\n  const [openState, setOpenState] = useState(false);\n  const open = openProp ?? openState;\n  const contentRef = useRef<HTMLDivElement>(null);\n  const [contentHeight, setContentHeight] = useState(0);\n  const hasRaw = children !== undefined && children !== null;\n  const Icon = KIND_ICON[kind];\n\n  const toggle = () => {\n    const next = !open;\n    setOpenState(next);\n    onOpenChange?.(next);\n  };\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={reduce ? { opacity: 0 } : { opacity: 0, y: 4 }}\n      animate={{ opacity: 1, y: 0 }}\n      transition={{ duration: reduce ? 0.15 : 0.25, ease: EASE_OUT }}\n      className={cn(\"relative flex gap-3 py-2\", className)}\n    >\n      <span\n        className={cn(\n          \"z-10 flex h-[23px] w-[23px] shrink-0 items-center justify-center rounded-full border bg-[var(--wb-surface)]\",\n          KIND_NODE_STYLE[kind],\n        )}\n      >\n        {active && !reduce ? (\n          <motion.span\n            aria-hidden\n            className=\"absolute h-[23px] w-[23px] rounded-full border border-[var(--wb-accent)]\"\n            initial={{ scale: 1, opacity: 0.5 }}\n            animate={{ scale: 1.5, opacity: 0 }}\n            transition={{ duration: 1.6, repeat: Infinity, ease: EASE_OUT }}\n          />\n        ) : null}\n        <span className=\"relative z-10 flex items-center justify-center\">\n          {icon ?? <Icon className=\"h-[13px] w-[13px]\" />}\n        </span>\n      </span>\n\n      <div className=\"min-w-0 flex-1\">\n        <div className=\"flex items-center gap-2\">\n          {active ? (\n            <TraceShimmerText className=\"text-sm\">{label}</TraceShimmerText>\n          ) : (\n            <span className=\"text-sm text-foreground\">{label}</span>\n          )}\n          {hasRaw ? (\n            <button\n              type=\"button\"\n              aria-expanded={open}\n              onClick={toggle}\n              className=\"text-xs text-muted-foreground transition-colors hover:text-foreground\"\n            >\n              View raw\n            </button>\n          ) : null}\n          {meta !== undefined && meta !== null ? (\n            <span className=\"ml-auto shrink-0 text-xs tabular-nums text-muted-foreground\">{meta}</span>\n          ) : null}\n        </div>\n        {detail !== undefined && detail !== null ? (\n          <div className=\"text-[13px] text-muted-foreground\">{detail}</div>\n        ) : null}\n        {hasRaw ? (\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 ref={contentRef} className=\"pt-1.5\">\n              {children}\n            </div>\n          </motion.div>\n        ) : null}\n      </div>\n    </motion.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 formatTraceCost(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 formatTraceTokenCount(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 TraceCostBadgeProps {\n  tokens?: number;\n  cost?: number;\n  className?: string;\n}\n\n/**\n * Compact usage pill sized for a `TraceStep`'s `meta` slot — token count\n * and/or cost, joined by a middle dot when both are passed. Deliberately\n * reimplements its own K/M and price formatting rather than importing the\n * thread block's, keeping this block self-contained.\n */\nexport function TraceCostBadge({ tokens, cost, className }: TraceCostBadgeProps) {\n  const fragments: string[] = [];\n  if (tokens !== undefined) fragments.push(`${formatTraceTokenCount(tokens)} tok`);\n  if (cost !== undefined) fragments.push(formatTraceCost(cost));\n\n  if (fragments.length === 0) return null;\n\n  return (\n    <span\n      className={cn(\n        \"inline-flex h-[18px] items-center gap-1 rounded-full bg-[var(--wb-inset-strong)] px-1.5 text-[10px] tabular-nums text-muted-foreground\",\n        className,\n      )}\n    >\n      {fragments.join(\" · \")}\n    </span>\n  );\n}\n\nexport interface TraceGroupProps {\n  /** Header line, e.g. \"Running 3 tools in parallel\". */\n  label: ReactNode;\n  children?: ReactNode;\n  className?: string;\n}\n\n/**\n * Parallel-execution group — a muted header line followed by a\n * dashed-ruled sub-list, indented to align under a `TraceStep`'s label\n * column. `children` are typically simplified rows the caller composes\n * (icon + text + meta) rather than full `TraceStep`s. Mounts as one entrance\n * unit, matching the other rail rows.\n */\nexport function TraceGroup({ label, children, className }: TraceGroupProps) {\n  const reduce = useReducedMotion() ?? false;\n\n  return (\n    <motion.div\n      initial={reduce ? { opacity: 0 } : { opacity: 0, y: 4 }}\n      animate={{ opacity: 1, y: 0 }}\n      transition={{ duration: reduce ? 0.15 : 0.25, ease: EASE_OUT }}\n      className={cn(\"relative flex flex-col py-2\", className)}\n    >\n      <div className=\"ml-9 text-[13px] text-muted-foreground\">{label}</div>\n      <div className=\"ml-9 mt-1 flex flex-col gap-1 border-[var(--wb-border)] border-l border-dashed pl-3\">\n        {children}\n      </div>\n    </motion.div>\n  );\n}\n\nexport interface TraceSubagentProps {\n  label: ReactNode;\n  /** Controlled expand state. */\n  open?: boolean;\n  onOpenChange?: (next: boolean) => void;\n  /** Nested `TraceStep`s — rendered inside a mini `Trace` of their own. */\n  children?: ReactNode;\n  className?: string;\n}\n\n/**\n * Nested sub-agent trace — a clickable header (chevron rotates on toggle)\n * that reveals its own indented `Trace` of steps below, styled like\n * `ThreadTurnHeader`'s hover pill. Height-measured expand/collapse mirrors\n * `ThreadCollapse` (`ResizeObserver`, 0 ↔ measured, `EASE_OUT` 0.25s,\n * instant under `useReducedMotion()`). Works controlled or uncontrolled.\n */\nexport function TraceSubagent({\n  label,\n  open: openProp,\n  onOpenChange,\n  children,\n  className,\n}: TraceSubagentProps) {\n  const reduce = useReducedMotion() ?? false;\n  const [openState, setOpenState] = useState(false);\n  const open = openProp ?? openState;\n  const contentRef = useRef<HTMLDivElement>(null);\n  const [contentHeight, setContentHeight] = useState(0);\n\n  const toggle = () => {\n    const next = !open;\n    setOpenState(next);\n    onOpenChange?.(next);\n  };\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={reduce ? { opacity: 0 } : { opacity: 0, y: 4 }}\n      animate={{ opacity: 1, y: 0 }}\n      transition={{ duration: reduce ? 0.15 : 0.25, ease: EASE_OUT }}\n      className={cn(\"relative flex flex-col py-2\", className)}\n    >\n      <button\n        type=\"button\"\n        aria-expanded={open}\n        onClick={toggle}\n        className=\"ml-9 inline-flex items-center gap-1 self-start rounded-lg px-1 py-0.5 text-sm text-foreground transition-colors hover:bg-[var(--wb-hover)]\"\n      >\n        {label}\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      <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 ref={contentRef} className=\"ml-9 border-[var(--wb-border)] border-l pl-4\">\n          <Trace>{children}</Trace>\n        </div>\n      </motion.div>\n    </motion.div>\n  );\n}\n\nexport interface TraceSummaryProps {\n  duration?: ReactNode;\n  tokens?: number;\n  cost?: number;\n  className?: string;\n  children?: ReactNode;\n}\n\n/**\n * Run-summary tail row appended below a trace's last step — total duration,\n * token count and cost, ruled off from the steps above. Mounts with the\n * same opacity + slide entrance as `TraceStep` (`EASE_OUT`, 0.25s), reduced\n * to an opacity-only fade under `useReducedMotion()`.\n */\nexport function TraceSummary({ duration, tokens, cost, className, children }: TraceSummaryProps) {\n  const reduce = useReducedMotion() ?? false;\n\n  return (\n    <motion.div\n      initial={reduce ? { opacity: 0 } : { opacity: 0, y: 4 }}\n      animate={{ opacity: 1, y: 0 }}\n      transition={{ duration: reduce ? 0.15 : 0.25, ease: EASE_OUT }}\n      className={cn(\n        \"ml-9 mt-1 flex items-center gap-3 border-[var(--wb-border)] border-t-[0.5px] pt-2 text-xs text-muted-foreground\",\n        className,\n      )}\n    >\n      {duration !== undefined && duration !== null ? (\n        <span className=\"flex items-center gap-1\">\n          <Clock className=\"h-3 w-3\" />\n          {duration}\n        </span>\n      ) : null}\n      {tokens !== undefined ? <span>{formatTraceTokenCount(tokens)} tokens</span> : null}\n      {cost !== undefined ? <span>{formatTraceCost(cost)}</span> : null}\n      {children}\n    </motion.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"}]}