{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"chat-app","type":"registry:component","title":"Chat App","description":"A complete agent conversation workspace composing navigation, messages, streaming, planning, approvals, tools, code, diffs, generated media, sources, and prompt input.","author":"Saurabh <saurabh10102@gmail.com>","dependencies":["clsx","lucide-react","motion","shiki","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/agents/chat-app.tsx","type":"registry:component","target":"@components/agents/chat-app.tsx","content":"\"use client\";\n// beui.dev/components/agents/chat-app\n\nimport type { ComponentProps } from \"react\";\nimport { AnimatedSidebarProvider } from \"@/components/motion/animated-sidebar\";\nimport { cn } from \"@/lib/utils\";\n\nexport type ChatAppProps = ComponentProps<typeof AnimatedSidebarProvider> & {\n  sidebarWidth?: string;\n};\n\nexport function ChatApp({\n  children,\n  className,\n  sidebarWidth = \"17rem\",\n  style,\n  ...props\n}: ChatAppProps) {\n  return (\n    <AnimatedSidebarProvider\n      {...props}\n      style={{ ...style, \"--sidebar-width\": sidebarWidth }}\n      className={cn(\n        \"min-h-0 w-full overflow-hidden rounded-2xl border border-border bg-background\",\n        className,\n      )}\n    >\n      {children}\n    </AnimatedSidebarProvider>\n  );\n}\n"},{"path":"components/agents/agent-activity/index.tsx","type":"registry:component","target":"@components/agents/agent-activity/index.tsx","content":"\"use client\";\n// beui.dev/components/agents/chat-app\n\nimport { ChevronDown } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport {\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { ThinkingShimmer } from \"@/components/agents/loading-states/thinking-shimmer\";\nimport { AgentDisclosure } from \"@/components/agents/agent-disclosure\";\nimport {\n  EASE_OUT,\n  SPRING_LAYOUT,\n  SPRING_SWAP,\n} from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\nimport { ActivityRow } from \"./activity-row\";\nimport type {\n  AgentActivityContentType,\n  AgentActivityItem,\n  AgentActivityProps,\n} from \"./types\";\n\nexport type {\n  AgentActivityContentType,\n  AgentActivityItem,\n  AgentActivityProps,\n  AgentActivitySearch,\n  AgentActivityStatus,\n  AgentActivityStep,\n  AgentActivityText,\n  AgentActivityTool,\n  AgentActivityTrace,\n  AgentSearchResult,\n  AgentStepStatus,\n  AgentTraceKind,\n} from \"./types\";\n\nfunction formatDuration(duration: number) {\n  const seconds = Math.max(0, Math.round(duration));\n  if (seconds < 60) return `${seconds}s`;\n\n  const minutes = Math.floor(seconds / 60);\n  const remainder = seconds % 60;\n  return remainder === 0 ? `${minutes}m` : `${minutes}m ${remainder}s`;\n}\n\nfunction useControllableOpen({\n  open,\n  defaultOpen,\n  onOpenChange,\n}: {\n  open?: boolean;\n  defaultOpen: boolean;\n  onOpenChange?: (open: boolean) => void;\n}) {\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const controlled = open !== undefined;\n  const currentOpen = open ?? internalOpen;\n\n  const setOpen = useCallback(\n    (next: boolean) => {\n      if (!controlled) setInternalOpen(next);\n      onOpenChange?.(next);\n    },\n    [controlled, onOpenChange],\n  );\n\n  return [currentOpen, setOpen] as const;\n}\n\nfunction getContentType(items: AgentActivityItem[]): AgentActivityContentType {\n  const first = items[0]?.type;\n  return first && items.every((item) => item.type === first) ? first : \"mixed\";\n}\n\nfunction getActiveLabel(type: AgentActivityContentType) {\n  if (type === \"search\") return \"Searching the web…\";\n  if (type === \"tool\") return \"Running tools…\";\n  if (type === \"trace\") return \"Working through the run…\";\n  if (type === \"mixed\") return \"Working through it…\";\n  return \"Thinking…\";\n}\n\nfunction getSummary(\n  type: AgentActivityContentType,\n  items: AgentActivityItem[],\n  duration: number,\n): ReactNode {\n  if (type === \"step\" || type === \"text\") {\n    return (\n      <>\n        Thought for <span className=\"tabular-nums\">{formatDuration(duration)}</span>\n      </>\n    );\n  }\n  if (type === \"search\") return \"Searched the web\";\n  if (type === \"tool\") {\n    return `Ran ${items.length} ${items.length === 1 ? \"tool\" : \"tools\"}`;\n  }\n  if (type === \"trace\") {\n    const messages = items.filter(\n      (item) =>\n        item.type === \"trace\" &&\n        (item.kind === \"thinking\" || item.kind === \"message\"),\n    ).length;\n    const tools = items.length - messages;\n    return `${tools} ${tools === 1 ? \"tool call\" : \"tool calls\"}, ${messages} ${messages === 1 ? \"message\" : \"messages\"}`;\n  }\n  return `Completed ${items.length} ${items.length === 1 ? \"step\" : \"steps\"}`;\n}\n\nexport function AgentActivity({\n  items,\n  contentType: initialContentType,\n  status = \"working\",\n  duration = 0,\n  open,\n  defaultOpen = false,\n  onOpenChange,\n  collapseOnComplete = true,\n  activeLabel,\n  summary,\n  maxHeight = 208,\n  className,\n  contentClassName,\n}: AgentActivityProps) {\n  const reduce = useReducedMotion() ?? false;\n  const baseId = useId();\n  const triggerId = `${baseId}-trigger`;\n  const contentId = `${baseId}-content`;\n  const contentRef = useRef<HTMLDivElement>(null);\n  const viewportRef = useRef<HTMLDivElement>(null);\n  const previousStatus = useRef(status);\n  const [contentHeight, setContentHeight] = useState(0);\n  const [currentOpen, setOpen] = useControllableOpen({\n    open,\n    defaultOpen,\n    onOpenChange,\n  });\n  const working = status === \"working\";\n  const expanded = working || currentOpen;\n  const contentType = items.length\n    ? getContentType(items)\n    : (initialContentType ?? \"mixed\");\n  const cappedHeight = Math.min(contentHeight, Math.max(0, maxHeight));\n  const viewportHeight = working ? Math.max(0, maxHeight) : cappedHeight;\n  const capped = contentHeight > maxHeight;\n  const streamOffset = working\n    ? Math.min(0, viewportHeight - contentHeight)\n    : 0;\n\n  useLayoutEffect(() => {\n    const node = contentRef.current;\n    if (!node) return;\n\n    const measure = () => setContentHeight(node.offsetHeight);\n    measure();\n\n    if (typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver(measure);\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, []);\n\n  useEffect(() => {\n    if (previousStatus.current === \"working\" && status === \"complete\") {\n      setOpen(!collapseOnComplete);\n    }\n    previousStatus.current = status;\n  }, [collapseOnComplete, setOpen, status]);\n\n  const toggle = () => {\n    const next = !currentOpen;\n    setOpen(next);\n    if (next) requestAnimationFrame(() => viewportRef.current?.scrollTo({ top: 0 }));\n  };\n\n  const liveLabel = activeLabel ?? getActiveLabel(contentType);\n  const completedSummary = summary ?? getSummary(contentType, items, duration);\n  const maskImage = capped\n    ? working\n      ? \"linear-gradient(to bottom, transparent, black 12px)\"\n      : \"linear-gradient(to bottom, transparent, black 12px, black calc(100% - 12px), transparent)\"\n    : undefined;\n\n  return (\n    <div\n      data-state={working ? \"working\" : expanded ? \"open\" : \"closed\"}\n      data-content={contentType}\n      aria-busy={working}\n      className={cn(\"w-full text-sm\", className)}\n    >\n      {working ? (\n        <div\n          id={triggerId}\n          role=\"status\"\n          className=\"flex h-7 min-w-0 items-center text-muted-foreground\"\n        >\n          <ThinkingShimmer>{liveLabel}</ThinkingShimmer>\n        </div>\n      ) : (\n        <button\n          id={triggerId}\n          type=\"button\"\n          aria-expanded={expanded}\n          aria-controls={contentId}\n          onClick={toggle}\n          className=\"group flex h-7 min-w-0 items-center gap-1.5 rounded-md text-left font-medium text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\"\n        >\n          <span className=\"truncate\">{completedSummary}</span>\n          <motion.span\n            aria-hidden=\"true\"\n            animate={{ rotate: expanded ? 180 : 0 }}\n            transition={reduce ? { duration: 0 } : SPRING_SWAP}\n            className=\"inline-flex shrink-0 text-muted-foreground/70 group-hover:text-foreground\"\n          >\n            <ChevronDown className=\"size-3.5\" />\n          </motion.span>\n        </button>\n      )}\n\n      <AgentDisclosure\n        id={contentId}\n        role=\"region\"\n        aria-labelledby={triggerId}\n        open={expanded}\n        openHeight={viewportHeight}\n      >\n        <div\n          ref={viewportRef}\n          className={cn(\n            \"scrollbar-hide pr-1\",\n            capped && expanded && !working ? \"overflow-y-auto\" : \"overflow-y-hidden\",\n          )}\n          style={{ height: viewportHeight, maskImage, WebkitMaskImage: maskImage }}\n        >\n          <motion.div\n            ref={contentRef}\n            role=\"list\"\n            initial={false}\n            animate={{ y: streamOffset }}\n            transition={reduce ? { duration: 0 } : SPRING_LAYOUT}\n            className={cn(\"space-y-0.5 py-2\", contentClassName)}\n          >\n            <AnimatePresence mode=\"popLayout\">\n              {items.map((item) => (\n                <motion.div\n                  layout=\"position\"\n                  key={item.id}\n                  role=\"listitem\"\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                  <ActivityRow item={item} />\n                </motion.div>\n              ))}\n            </AnimatePresence>\n          </motion.div>\n        </div>\n      </AgentDisclosure>\n    </div>\n  );\n}\n"},{"path":"components/agents/ai-sidebar.tsx","type":"registry:component","target":"@components/agents/ai-sidebar.tsx","content":"\"use client\";\n// beui.dev/components/agents/chat-app\n\nimport {\n  Bookmark,\n  FileText,\n  Folder,\n  FolderOpen,\n  MoreHorizontal,\n  Pencil,\n} from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport {\n  type DragEvent,\n  type KeyboardEvent,\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport {\n  MorphPopover,\n  MorphPopoverContent,\n  MorphPopoverTrigger,\n} from \"@/components/motion/popover-morph\";\nimport { EASE_OUT, SPRING_LAYOUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport type SidebarResourceKind =\n  | \"folder\"\n  | \"project\"\n  | \"file\"\n  | \"bookmark\";\n\nexport interface SidebarResource {\n  id: string;\n  label: string;\n  kind: SidebarResourceKind;\n  children?: SidebarResource[];\n  disabled?: boolean;\n}\n\nexport type SidebarResourceDropPosition = \"before\" | \"inside\" | \"after\";\n\nexport interface SidebarResourceMove {\n  itemId: string;\n  targetId: string | null;\n  position: SidebarResourceDropPosition;\n}\n\nexport interface SidebarResourceMenuControls {\n  close: () => void;\n  rename: () => void;\n}\n\nexport interface AISidebarProps {\n  items?: SidebarResource[];\n  defaultItems?: SidebarResource[];\n  onItemsChange?: (items: SidebarResource[]) => void;\n  /** Reject the promise to roll the optimistic move back. */\n  onMove?: (move: SidebarResourceMove) => void | Promise<void>;\n  onMoveError?: (error: unknown, move: SidebarResourceMove) => void;\n  onRename?: (item: SidebarResource, label: string) => void | Promise<void>;\n  activeId?: string | null;\n  defaultActiveId?: string | null;\n  onActiveChange?: (id: string) => void;\n  defaultExpandedIds?: string[];\n  renderIcon?: (item: SidebarResource) => ReactNode;\n  renderMenu?: (\n    item: SidebarResource,\n    controls: SidebarResourceMenuControls,\n  ) => ReactNode;\n  ariaLabel?: string;\n  className?: string;\n}\n\ninterface FlatResource {\n  item: SidebarResource;\n  depth: number;\n  parentId: string | null;\n}\n\ninterface DropTarget {\n  id: string | null;\n  position: SidebarResourceDropPosition;\n}\n\nconst ROW_REVEAL = {\n  duration: 0.16,\n  ease: EASE_OUT,\n} as const;\n\nfunction canContain(item: SidebarResource) {\n  return item.kind === \"folder\" || item.kind === \"project\";\n}\n\nfunction flattenResources(\n  items: SidebarResource[],\n  expanded: Set<string>,\n  depth = 0,\n  parentId: string | null = null,\n): FlatResource[] {\n  return items.flatMap((item) => {\n    const row = { item, depth, parentId };\n    if (!item.children?.length || !expanded.has(item.id)) return [row];\n    return [\n      row,\n      ...flattenResources(item.children, expanded, depth + 1, item.id),\n    ];\n  });\n}\n\nfunction findResource(\n  items: SidebarResource[],\n  id: string,\n): SidebarResource | undefined {\n  for (const item of items) {\n    if (item.id === id) return item;\n    const child = item.children ? findResource(item.children, id) : undefined;\n    if (child) return child;\n  }\n}\n\nfunction containsResource(item: SidebarResource, id: string): boolean {\n  return (\n    item.id === id ||\n    item.children?.some((child) => containsResource(child, id)) === true\n  );\n}\n\nfunction removeResource(\n  items: SidebarResource[],\n  id: string,\n): { items: SidebarResource[]; removed?: SidebarResource } {\n  let removed: SidebarResource | undefined;\n  const next: SidebarResource[] = [];\n\n  for (const item of items) {\n    if (item.id === id) {\n      removed = item;\n      continue;\n    }\n\n    if (item.children?.length) {\n      const childResult = removeResource(item.children, id);\n      if (childResult.removed) {\n        removed = childResult.removed;\n        next.push({ ...item, children: childResult.items });\n        continue;\n      }\n    }\n\n    next.push(item);\n  }\n\n  return { items: next, removed };\n}\n\nfunction insertResource(\n  items: SidebarResource[],\n  resource: SidebarResource,\n  targetId: string | null,\n  position: SidebarResourceDropPosition,\n): SidebarResource[] {\n  if (targetId === null) return [...items, resource];\n\n  const next: SidebarResource[] = [];\n  for (const item of items) {\n    if (item.id === targetId) {\n      if (position === \"before\") next.push(resource, item);\n      else if (position === \"after\") next.push(item, resource);\n      else next.push({ ...item, children: [...(item.children ?? []), resource] });\n      continue;\n    }\n\n    if (item.children?.length) {\n      next.push({\n        ...item,\n        children: insertResource(item.children, resource, targetId, position),\n      });\n    } else {\n      next.push(item);\n    }\n  }\n  return next;\n}\n\nfunction moveResource(\n  items: SidebarResource[],\n  move: SidebarResourceMove,\n): SidebarResource[] | null {\n  const source = findResource(items, move.itemId);\n  if (!source || source.disabled) return null;\n  if (move.targetId && containsResource(source, move.targetId)) return null;\n\n  const target = move.targetId ? findResource(items, move.targetId) : undefined;\n  if (\n    move.position === \"inside\" &&\n    (!target || target.disabled || !canContain(target))\n  )\n    return null;\n\n  const removed = removeResource(items, move.itemId);\n  if (!removed.removed) return null;\n  return insertResource(\n    removed.items,\n    removed.removed,\n    move.targetId,\n    move.position,\n  );\n}\n\nfunction renameResource(\n  items: SidebarResource[],\n  id: string,\n  label: string,\n): SidebarResource[] {\n  return items.map((item) => ({\n    ...item,\n    label: item.id === id ? label : item.label,\n    children: item.children\n      ? renameResource(item.children, id, label)\n      : undefined,\n  }));\n}\n\nfunction defaultIcon(item: SidebarResource, expanded: boolean) {\n  const Icon =\n    item.kind === \"folder\" || item.kind === \"project\"\n      ? expanded\n        ? FolderOpen\n        : Folder\n      : item.kind === \"bookmark\"\n          ? Bookmark\n          : FileText;\n  return <Icon className=\"size-4\" />;\n}\n\nfunction MarqueeLabel({ active, children }: { active: boolean; children: string }) {\n  const reduce = useReducedMotion() ?? false;\n  const viewportRef = useRef<HTMLSpanElement>(null);\n  const labelRef = useRef<HTMLSpanElement>(null);\n  const [distance, setDistance] = useState(0);\n\n  useEffect(() => {\n    const measure = () => {\n      const viewport = viewportRef.current;\n      const label = labelRef.current;\n      if (!viewport || !label) return;\n      setDistance(label.scrollWidth > viewport.clientWidth ? label.scrollWidth + 24 : 0);\n    };\n    measure();\n    const observer = new ResizeObserver(measure);\n    if (viewportRef.current) observer.observe(viewportRef.current);\n    if (labelRef.current) observer.observe(labelRef.current);\n    return () => observer.disconnect();\n  }, []);\n\n  const running = active && distance > 0 && !reduce;\n\n  return (\n    <span ref={viewportRef} className=\"block min-w-0 flex-1 overflow-hidden\">\n      <motion.span\n        className=\"flex w-max items-center gap-6 whitespace-nowrap\"\n        animate={{ x: running ? [0, -distance] : 0 }}\n        transition={\n          running\n            ? {\n                duration: Math.max(2.4, distance / 34),\n                ease: \"linear\",\n                repeat: Number.POSITIVE_INFINITY,\n                repeatDelay: 2,\n              }\n            : ROW_REVEAL\n        }\n      >\n        <span ref={labelRef}>{children}</span>\n        {running ? <span aria-hidden=\"true\">{children}</span> : null}\n      </motion.span>\n    </span>\n  );\n}\n\ninterface ResourceRowProps {\n  row: FlatResource;\n  active: boolean;\n  expanded: boolean;\n  focused: boolean;\n  draggingId: string | null;\n  dropTarget: DropTarget | null;\n  menuOpen: boolean;\n  renaming: boolean;\n  onDragEnd: () => void;\n  onDragOver: (event: DragEvent<HTMLDivElement>, row: FlatResource) => void;\n  onDragStart: (event: DragEvent<HTMLDivElement>, id: string) => void;\n  onDrop: (event: DragEvent<HTMLDivElement>) => void;\n  onFocus: () => void;\n  onKeyDown: (event: KeyboardEvent<HTMLDivElement>) => void;\n  onMenuOpenChange: (open: boolean) => void;\n  onRenameCancel: () => void;\n  onRenameCommit: (label: string) => void;\n  onRenameStart: () => void;\n  onSelect: () => void;\n  onToggle: () => void;\n  renderIcon?: (item: SidebarResource) => ReactNode;\n  renderMenu?: AISidebarProps[\"renderMenu\"];\n  setRef: (node: HTMLDivElement | null) => void;\n}\n\nfunction ResourceRow({\n  row,\n  active,\n  expanded,\n  focused,\n  draggingId,\n  dropTarget,\n  menuOpen,\n  renaming,\n  onDragEnd,\n  onDragOver,\n  onDragStart,\n  onDrop,\n  onFocus,\n  onKeyDown,\n  onMenuOpenChange,\n  onRenameCancel,\n  onRenameCommit,\n  onRenameStart,\n  onSelect,\n  onToggle,\n  renderIcon,\n  renderMenu,\n  setRef,\n}: ResourceRowProps) {\n  const reduce = useReducedMotion() ?? false;\n  const [hovered, setHovered] = useState(false);\n  const inputRef = useRef<HTMLInputElement>(null);\n  const skipRenameBlurRef = useRef(false);\n  const draggedRef = useRef(false);\n  const [draft, setDraft] = useState(row.item.label);\n  const acceptsChildren = canContain(row.item);\n  const isDragging = draggingId === row.item.id;\n  const dropPosition = dropTarget?.id === row.item.id ? dropTarget.position : null;\n\n  useEffect(() => {\n    if (!renaming) return;\n    skipRenameBlurRef.current = false;\n    setDraft(row.item.label);\n    requestAnimationFrame(() => {\n      inputRef.current?.focus();\n      inputRef.current?.select();\n    });\n  }, [renaming, row.item.label]);\n\n  const menu = renderMenu?.(row.item, {\n    close: () => onMenuOpenChange(false),\n    rename: () => {\n      onMenuOpenChange(false);\n      onRenameStart();\n    },\n  }) ?? (\n    <button\n      type=\"button\"\n      onClick={() => {\n        onMenuOpenChange(false);\n        onRenameStart();\n      }}\n      className=\"flex h-8 w-full items-center gap-2 rounded-lg px-2.5 text-left text-xs text-foreground outline-none transition-colors hover:bg-muted focus-visible:bg-muted focus-visible:ring-2 focus-visible:ring-ring\"\n    >\n      <Pencil aria-hidden=\"true\" className=\"size-3.5\" />\n      Rename\n    </button>\n  );\n\n  return (\n    <motion.div\n      ref={setRef}\n      layout=\"position\"\n      transition={reduce ? { duration: 0 } : SPRING_LAYOUT}\n      role=\"treeitem\"\n      aria-level={row.depth + 1}\n      aria-selected={acceptsChildren ? undefined : active}\n      aria-expanded={acceptsChildren ? expanded : undefined}\n      aria-disabled={row.item.disabled || undefined}\n      tabIndex={focused ? 0 : -1}\n      draggable={!row.item.disabled && !renaming}\n      data-menu-open={menuOpen || undefined}\n      data-drop={dropPosition ?? undefined}\n      data-dragging={isDragging || undefined}\n      onFocus={onFocus}\n      onKeyDown={onKeyDown}\n      onClick={(event) => {\n        if (\n          event.defaultPrevented ||\n          draggedRef.current ||\n          renaming ||\n          row.item.disabled\n        )\n          return;\n        if (acceptsChildren) onToggle();\n        else onSelect();\n      }}\n      onDoubleClick={(event) => {\n        if (acceptsChildren || row.item.disabled) return;\n        event.preventDefault();\n        onRenameStart();\n      }}\n      onMouseEnter={() => setHovered(true)}\n      onMouseLeave={() => setHovered(false)}\n      onDragStartCapture={(event) => {\n        draggedRef.current = true;\n        onDragStart(event, row.item.id);\n      }}\n      onDragEndCapture={() => {\n        onDragEnd();\n        requestAnimationFrame(() => {\n          draggedRef.current = false;\n        });\n      }}\n      onDragOver={(event) => onDragOver(event, row)}\n      onDrop={onDrop}\n      className={cn(\n        \"group/resource relative flex min-h-9 min-w-0 cursor-pointer items-center gap-2.5 rounded-xl pr-3 text-sm outline-none\",\n        \"text-muted-foreground transition-colors hover:bg-muted hover:text-foreground\",\n        \"focus-visible:bg-muted/70 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset\",\n        \"data-[menu-open=true]:bg-muted data-[menu-open=true]:text-foreground\",\n        \"data-[dragging=true]:opacity-40\",\n        \"data-[drop=inside]:bg-primary/10 data-[drop=inside]:ring-1 data-[drop=inside]:ring-primary/45\",\n        \"data-[drop=before]:before:absolute data-[drop=before]:before:-top-0.5 data-[drop=before]:before:right-2 data-[drop=before]:before:left-2 data-[drop=before]:before:h-0.5 data-[drop=before]:before:rounded-full data-[drop=before]:before:bg-primary\",\n        \"data-[drop=after]:after:absolute data-[drop=after]:after:-bottom-0.5 data-[drop=after]:after:right-2 data-[drop=after]:after:left-2 data-[drop=after]:after:h-0.5 data-[drop=after]:after:rounded-full data-[drop=after]:after:bg-primary\",\n        !acceptsChildren && active && \"bg-muted text-foreground\",\n        row.item.disabled && \"cursor-not-allowed opacity-45\",\n      )}\n      style={{ paddingLeft: `${12 + row.depth * 16}px` }}\n    >\n      <span aria-hidden=\"true\" className=\"grid size-5 shrink-0 place-items-center\">\n        {renderIcon?.(row.item) ?? defaultIcon(row.item, expanded)}\n      </span>\n\n      {renaming ? (\n        <input\n          ref={inputRef}\n          value={draft}\n          aria-label={`Rename ${row.item.label}`}\n          onChange={(event) => setDraft(event.target.value)}\n          draggable={false}\n          onClick={(event) => event.stopPropagation()}\n          onDoubleClick={(event) => event.stopPropagation()}\n          onBlur={() => {\n            if (!skipRenameBlurRef.current) onRenameCommit(draft);\n          }}\n          onKeyDown={(event) => {\n            event.stopPropagation();\n            if (event.key === \"Enter\") {\n              skipRenameBlurRef.current = true;\n              onRenameCommit(draft);\n            }\n            if (event.key === \"Escape\") {\n              skipRenameBlurRef.current = true;\n              onRenameCancel();\n            }\n          }}\n          className=\"mx-1 h-7 min-w-0 flex-1 rounded-md border border-border bg-background px-2 text-sm text-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n        />\n      ) : (\n        <MarqueeLabel active={hovered || menuOpen}>{row.item.label}</MarqueeLabel>\n      )}\n\n      {!renaming && !row.item.disabled ? (\n        <MorphPopover\n          open={menuOpen}\n          onOpenChange={onMenuOpenChange}\n        >\n          <MorphPopoverTrigger>\n            <button\n              type=\"button\"\n              draggable={false}\n              tabIndex={-1}\n              aria-label={`Actions for ${row.item.label}`}\n              onClick={(event) => event.stopPropagation()}\n              className=\"grid size-7 shrink-0 place-items-center rounded-lg opacity-0 outline-none transition-opacity hover:bg-foreground/5 focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-ring group-hover/resource:opacity-100 group-data-[menu-open=true]/resource:opacity-100\"\n            >\n              <MoreHorizontal aria-hidden=\"true\" className=\"size-4\" />\n            </button>\n          </MorphPopoverTrigger>\n          <MorphPopoverContent\n            side=\"bottom\"\n            align=\"end\"\n            sideOffset={8}\n            radius={12}\n            className=\"w-40 p-1.5\"\n          >\n            <div data-sidebar-resource-menu={row.item.id}>{menu}</div>\n          </MorphPopoverContent>\n        </MorphPopover>\n      ) : null}\n    </motion.div>\n  );\n}\n\nexport function AISidebar({\n  items,\n  defaultItems = [],\n  onItemsChange,\n  onMove,\n  onMoveError,\n  onRename,\n  activeId,\n  defaultActiveId = null,\n  onActiveChange,\n  defaultExpandedIds = [],\n  renderIcon,\n  renderMenu,\n  ariaLabel = \"Resources\",\n  className,\n}: AISidebarProps) {\n  const [internalItems, setInternalItems] = useState(items ?? defaultItems);\n  const [internalActiveId, setInternalActiveId] = useState(defaultActiveId);\n  const [expandedIds, setExpandedIds] = useState(\n    () => new Set(defaultExpandedIds),\n  );\n  const [focusedId, setFocusedId] = useState<string | null>(\n    activeId ?? defaultActiveId,\n  );\n  const [draggingId, setDraggingId] = useState<string | null>(null);\n  const [dropTarget, setDropTarget] = useState<DropTarget | null>(null);\n  const [menuOpenId, setMenuOpenId] = useState<string | null>(null);\n  const [renamingId, setRenamingId] = useState<string | null>(null);\n  const [announcement, setAnnouncement] = useState(\"\");\n  const rowRefs = useRef(new Map<string, HTMLDivElement>());\n  const movePendingRef = useRef(false);\n  const renderedItems = internalItems;\n  const selectedId = activeId ?? internalActiveId;\n\n  useEffect(() => {\n    if (items) setInternalItems(items);\n  }, [items]);\n\n  const flat = useMemo(\n    () => flattenResources(renderedItems, expandedIds),\n    [expandedIds, renderedItems],\n  );\n\n  useEffect(() => {\n    if (focusedId && flat.some((row) => row.item.id === focusedId)) return;\n    setFocusedId(flat[0]?.item.id ?? null);\n  }, [flat, focusedId]);\n\n  useEffect(() => {\n    if (!menuOpenId) return;\n    const frame = requestAnimationFrame(() => {\n      const menus = Array.from(\n        document.querySelectorAll<HTMLElement>(\"[data-sidebar-resource-menu]\"),\n      );\n      menus\n        .find((menu) => menu.dataset.sidebarResourceMenu === menuOpenId)\n        ?.querySelector<HTMLElement>(\"button, a[href]\")\n        ?.focus();\n    });\n    return () => cancelAnimationFrame(frame);\n  }, [menuOpenId]);\n\n  const updateItems = useCallback(\n    (next: SidebarResource[]) => {\n      setInternalItems(next);\n      onItemsChange?.(next);\n    },\n    [onItemsChange],\n  );\n\n  const performMove = useCallback(\n    async (move: SidebarResourceMove) => {\n      if (movePendingRef.current) {\n        setAnnouncement(\"Wait for the current move to finish.\");\n        return;\n      }\n      const before = renderedItems;\n      const next = moveResource(before, move);\n      if (!next || next === before) return;\n\n      movePendingRef.current = true;\n      updateItems(next);\n      setDropTarget(null);\n      setDraggingId(null);\n      const moved = findResource(before, move.itemId);\n      const target = move.targetId ? findResource(before, move.targetId) : null;\n      setAnnouncement(\n        target\n          ? `Moved ${moved?.label ?? \"item\"} ${move.position} ${target.label}.`\n          : `Moved ${moved?.label ?? \"item\"} to the top level.`,\n      );\n\n      try {\n        await onMove?.(move);\n      } catch (error) {\n        updateItems(before);\n        setAnnouncement(`Move failed. ${moved?.label ?? \"Item\"} was restored.`);\n        onMoveError?.(error, move);\n      } finally {\n        movePendingRef.current = false;\n      }\n    },\n    [onMove, onMoveError, renderedItems, updateItems],\n  );\n\n  const focusRow = useCallback((id: string) => {\n    setFocusedId(id);\n    requestAnimationFrame(() => rowRefs.current.get(id)?.focus());\n  }, []);\n\n  const select = useCallback(\n    (id: string) => {\n      if (activeId === undefined) setInternalActiveId(id);\n      onActiveChange?.(id);\n    },\n    [activeId, onActiveChange],\n  );\n\n  const toggle = useCallback((id: string) => {\n    setExpandedIds((current) => {\n      const next = new Set(current);\n      if (next.has(id)) next.delete(id);\n      else next.add(id);\n      return next;\n    });\n  }, []);\n\n  const handleKeyDown = useCallback(\n    (event: KeyboardEvent<HTMLDivElement>, row: FlatResource) => {\n      const index = flat.findIndex(({ item }) => item.id === row.item.id);\n      const previous = flat[index - 1];\n      const next = flat[index + 1];\n      const moveModifier = event.altKey && event.shiftKey;\n\n      if (event.key === \"ArrowDown\" && !moveModifier && next) {\n        event.preventDefault();\n        focusRow(next.item.id);\n        return;\n      }\n      if (event.key === \"ArrowUp\" && !moveModifier && previous) {\n        event.preventDefault();\n        focusRow(previous.item.id);\n        return;\n      }\n      if (event.key === \"Home\" && flat[0]) {\n        event.preventDefault();\n        focusRow(flat[0].item.id);\n        return;\n      }\n      if (event.key === \"End\" && flat.at(-1)) {\n        event.preventDefault();\n        focusRow(flat.at(-1)?.item.id ?? row.item.id);\n        return;\n      }\n\n      if (row.item.disabled) {\n        if (event.key === \"ArrowLeft\" && row.parentId) {\n          event.preventDefault();\n          focusRow(row.parentId);\n        } else if (\n          moveModifier ||\n          [\"ArrowRight\", \"Enter\", \" \", \"F2\", \"ContextMenu\"].includes(\n            event.key,\n          ) ||\n          (event.shiftKey && event.key === \"F10\")\n        ) {\n          event.preventDefault();\n        }\n        return;\n      }\n\n      if (moveModifier && event.key === \"ArrowUp\" && previous) {\n        event.preventDefault();\n        void performMove({ itemId: row.item.id, targetId: previous.item.id, position: \"before\" });\n        return;\n      }\n      if (moveModifier && event.key === \"ArrowDown\" && next) {\n        event.preventDefault();\n        void performMove({ itemId: row.item.id, targetId: next.item.id, position: \"after\" });\n        return;\n      }\n      if (moveModifier && event.key === \"ArrowRight\" && previous && canContain(previous.item)) {\n        event.preventDefault();\n        setExpandedIds((current) => new Set(current).add(previous.item.id));\n        void performMove({ itemId: row.item.id, targetId: previous.item.id, position: \"inside\" });\n        return;\n      }\n      if (moveModifier && event.key === \"ArrowLeft\" && row.parentId) {\n        event.preventDefault();\n        void performMove({ itemId: row.item.id, targetId: row.parentId, position: \"after\" });\n        return;\n      }\n\n      if (event.key === \"ArrowRight\" && canContain(row.item)) {\n        event.preventDefault();\n        if (!expandedIds.has(row.item.id)) toggle(row.item.id);\n        else if (next?.parentId === row.item.id) focusRow(next.item.id);\n      } else if (event.key === \"ArrowLeft\") {\n        event.preventDefault();\n        if (expandedIds.has(row.item.id)) toggle(row.item.id);\n        else if (row.parentId) focusRow(row.parentId);\n      } else if (event.key === \"Enter\" || event.key === \" \") {\n        event.preventDefault();\n        if (canContain(row.item)) toggle(row.item.id);\n        else select(row.item.id);\n      } else if (event.key === \"F2\") {\n        event.preventDefault();\n        setRenamingId(row.item.id);\n      } else if (event.key === \"ContextMenu\" || (event.shiftKey && event.key === \"F10\")) {\n        event.preventDefault();\n        setMenuOpenId(row.item.id);\n      }\n    },\n    [expandedIds, flat, focusRow, performMove, select, toggle],\n  );\n\n  return (\n    <>\n      <div\n      role=\"tree\"\n      aria-label={ariaLabel}\n      aria-multiselectable=\"false\"\n      onDragOver={(event) => {\n        if (!draggingId || event.target !== event.currentTarget) return;\n        event.preventDefault();\n        setDropTarget({ id: null, position: \"after\" });\n      }}\n      onDrop={(event) => {\n        event.preventDefault();\n        if (draggingId && dropTarget) {\n          void performMove({\n            itemId: draggingId,\n            targetId: dropTarget.id,\n            position: dropTarget.position,\n          });\n        }\n      }}\n      className={cn(\n        \"relative flex min-w-0 flex-col gap-0.5 [overflow-anchor:none] group-data-[state=collapsed]/sidebar:hidden\",\n        draggingId && \"select-none pb-9\",\n        className,\n      )}\n    >\n      <AnimatePresence initial={false}>\n        {flat.map((row) => (\n          <ResourceRow\n            key={row.item.id}\n            row={row}\n            active={selectedId === row.item.id}\n            expanded={expandedIds.has(row.item.id)}\n            focused={focusedId === row.item.id}\n            draggingId={draggingId}\n            dropTarget={dropTarget}\n            menuOpen={menuOpenId === row.item.id}\n            renaming={renamingId === row.item.id}\n            onFocus={() => setFocusedId(row.item.id)}\n            onSelect={() => select(row.item.id)}\n            onToggle={() => toggle(row.item.id)}\n            onKeyDown={(event) => handleKeyDown(event, row)}\n            onRenameStart={() => setRenamingId(row.item.id)}\n            onRenameCancel={() => setRenamingId(null)}\n            onRenameCommit={(label) => {\n              const trimmed = label.trim();\n              setRenamingId(null);\n              if (!trimmed || trimmed === row.item.label) return;\n              const before = renderedItems;\n              updateItems(renameResource(before, row.item.id, trimmed));\n              void Promise.resolve(onRename?.(row.item, trimmed)).catch(() => {\n                updateItems(before);\n                setAnnouncement(`Rename failed. ${row.item.label} was restored.`);\n              });\n            }}\n            onMenuOpenChange={(open) => {\n              setMenuOpenId(open ? row.item.id : null);\n              if (!open) focusRow(row.item.id);\n            }}\n            onDragStart={(event, id) => {\n              setDraggingId(id);\n              event.dataTransfer.effectAllowed = \"move\";\n              event.dataTransfer.setData(\"text/plain\", id);\n            }}\n            onDragEnd={() => {\n              setDraggingId(null);\n              setDropTarget(null);\n            }}\n            onDragOver={(event, targetRow) => {\n              if (!draggingId || draggingId === targetRow.item.id) return;\n              const source = findResource(renderedItems, draggingId);\n              if (source && containsResource(source, targetRow.item.id)) return;\n              event.preventDefault();\n              event.stopPropagation();\n              const rect = event.currentTarget.getBoundingClientRect();\n              const ratio = (event.clientY - rect.top) / rect.height;\n              const position =\n                !targetRow.item.disabled &&\n                canContain(targetRow.item) &&\n                ratio >= 0.25 &&\n                ratio <= 0.75\n                  ? \"inside\"\n                  : ratio < 0.5\n                    ? \"before\"\n                    : \"after\";\n              setDropTarget({ id: targetRow.item.id, position });\n            }}\n            onDrop={(event) => {\n              event.preventDefault();\n              event.stopPropagation();\n              if (draggingId && dropTarget) {\n                void performMove({\n                  itemId: draggingId,\n                  targetId: dropTarget.id,\n                  position: dropTarget.position,\n                });\n              }\n            }}\n            renderIcon={renderIcon}\n            renderMenu={renderMenu}\n            setRef={(node) => {\n              if (node) rowRefs.current.set(row.item.id, node);\n              else rowRefs.current.delete(row.item.id);\n            }}\n          />\n        ))}\n      </AnimatePresence>\n\n      {draggingId ? (\n        <div\n          aria-hidden=\"true\"\n          data-active={dropTarget?.id === null || undefined}\n          className=\"absolute inset-x-1 bottom-0 flex h-8 items-center justify-center rounded-lg border border-dashed border-border text-[10px] text-muted-foreground data-[active=true]:border-primary/50 data-[active=true]:bg-primary/10 data-[active=true]:text-foreground\"\n        >\n          Move to top level\n        </div>\n      ) : null}\n\n      </div>\n      <span className=\"sr-only\" aria-live=\"polite\">\n        {announcement}\n      </span>\n    </>\n  );\n}\n"},{"path":"components/agents/approval-card/index.tsx","type":"registry:component","target":"@components/agents/approval-card/index.tsx","content":"\"use client\";\n// beui.dev/components/agents/chat-app\n\nimport {\n  ArrowLeft,\n  ArrowRight,\n  Check,\n  CircleHelp,\n  LoaderCircle,\n  MessageSquareText,\n  X,\n} from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { AgentDisclosure } from \"@/components/agents/agent-disclosure\";\nimport { ActionSwapRollText } from \"@/components/motion/action-swap-roll\";\nimport { Button } from \"@/components/motion/button\";\nimport { Checkbox } from \"@/components/motion/checkbox\";\nimport { Input } from \"@/components/motion/input\";\nimport { RadioGroup, RadioGroupItem } from \"@/components/motion/radio\";\nimport { EASE_OUT, SPRING_SWAP } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\nimport type {\n  ApprovalCardAnswer,\n  ApprovalCardAnswers,\n  ApprovalCardProps,\n  ApprovalCardQuestion,\n  ApprovalCardStatus,\n} from \"./types\";\n\nexport type {\n  ApprovalCardAnswer,\n  ApprovalCardAnswers,\n  ApprovalCardOption,\n  ApprovalCardProps,\n  ApprovalCardQuestion,\n  ApprovalCardStatus,\n} from \"./types\";\n\nconst EMPTY_ANSWER: ApprovalCardAnswer = { selected: [], custom: \"\" };\n\nfunction getStatusLabel(status: ApprovalCardStatus) {\n  if (status === \"submitting\") return \"Submitting\";\n  if (status === \"approved\") return \"Approved\";\n  if (status === \"rejected\") return \"Rejected\";\n  if (status === \"changes-requested\") return \"Changes requested\";\n  if (status === \"answered\") return \"Response submitted\";\n  return \"Input required\";\n}\n\nfunction getStatusClass(status: ApprovalCardStatus) {\n  if (status === \"approved\" || status === \"answered\") {\n    return \"text-emerald-600 dark:text-emerald-400\";\n  }\n  if (status === \"rejected\") return \"text-rose-600 dark:text-rose-400\";\n  if (status === \"changes-requested\") {\n    return \"text-amber-600 dark:text-amber-400\";\n  }\n  return \"text-muted-foreground\";\n}\n\nfunction getStatusBadgeClass(status: ApprovalCardStatus) {\n  if (status === \"pending\" || status === \"changes-requested\") {\n    return \"border-amber-500/30 bg-amber-500/10 text-amber-600 dark:text-amber-400\";\n  }\n  if (status === \"submitting\") {\n    return \"border-blue-500/30 bg-blue-500/10 text-blue-600 dark:text-blue-400\";\n  }\n  if (status === \"approved\" || status === \"answered\") {\n    return \"border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400\";\n  }\n  return \"border-rose-500/30 bg-rose-500/10 text-rose-600 dark:text-rose-400\";\n}\n\nfunction isAnswered(answer: ApprovalCardAnswer) {\n  return answer.selected.length > 0 || Boolean(answer.custom?.trim());\n}\n\nfunction QuestionOptions({\n  question,\n  answer,\n  disabled,\n  onChange,\n  onSingleSelect,\n}: {\n  question: ApprovalCardQuestion;\n  answer: ApprovalCardAnswer;\n  disabled: boolean;\n  onChange: (answer: ApprovalCardAnswer) => void;\n  onSingleSelect?: () => void;\n}) {\n  const custom = answer.custom ?? \"\";\n\n  return (\n    <div className=\"mt-3\">\n      {question.options?.length ? (\n        question.multiple ? (\n          <div className=\"grid gap-0.5\">\n            {question.options.map((option) => (\n              <Checkbox\n                key={option.value}\n                checked={answer.selected.includes(option.value)}\n                disabled={disabled || option.disabled}\n                label={option.label}\n                onCheckedChange={(checked) =>\n                  onChange({\n                    ...answer,\n                    selected: checked\n                      ? [...answer.selected, option.value]\n                      : answer.selected.filter((value) => value !== option.value),\n                  })\n                }\n                className=\"min-h-9 rounded-lg px-1.5 py-1\"\n              />\n            ))}\n          </div>\n        ) : (\n          <RadioGroup\n            value={answer.selected[0] ?? \"\"}\n            onValueChange={(value) => {\n              onChange({ selected: [value], custom: \"\" });\n              onSingleSelect?.();\n            }}\n            className=\"gap-0.5\"\n          >\n            {question.options.map((option) => (\n              <RadioGroupItem\n                key={option.value}\n                value={option.value}\n                label={option.label}\n                disabled={disabled || option.disabled}\n                className=\"min-h-9 rounded-lg px-1.5 py-1\"\n              />\n            ))}\n          </RadioGroup>\n        )\n      ) : null}\n\n      {question.allowCustom ? (\n        <Input\n          value={custom}\n          disabled={disabled}\n          placeholder={question.customPlaceholder ?? \"Add another response…\"}\n          onChange={(value) =>\n            onChange({\n              selected: question.multiple ? answer.selected : [],\n              custom: value,\n            })\n          }\n          className={cn(\"p-0.5\", question.options?.length && \"mt-1.5\")}\n          classNames={{\n            field:\n              \"h-10 rounded-xl border-0 bg-background/70 focus-within:bg-background\",\n            input: \"px-3 text-sm\",\n          }}\n        />\n      ) : null}\n    </div>\n  );\n}\n\nfunction ProgressDots({ current, ids }: { current: number; ids: string[] }) {\n  return (\n    <span className=\"flex gap-1.5\">\n      <span className=\"sr-only\">\n        Question {current + 1} of {ids.length}\n      </span>\n      {ids.map((id, index) => (\n        <motion.span\n          key={id}\n          aria-hidden=\"true\"\n          initial={{\n            scale: index === current ? 1 : 0.75,\n            opacity: index <= current ? 1 : 0.35,\n          }}\n          animate={{\n            scale: index === current ? 1 : 0.75,\n            opacity: index <= current ? 1 : 0.35,\n          }}\n          transition={SPRING_SWAP}\n          className=\"size-1.5 rounded-full bg-foreground\"\n        />\n      ))}\n    </span>\n  );\n}\n\nexport function ApprovalCard({\n  title = \"Approval required\",\n  description,\n  children,\n  questions = [],\n  status = \"pending\",\n  answers,\n  defaultAnswers = {},\n  onAnswersChange,\n  step,\n  defaultStep = 0,\n  onStepChange,\n  onSubmit,\n  onApprove,\n  onReject,\n  onRequestChanges,\n  onDismiss,\n  approveLabel = \"Approve\",\n  submitLabel = \"Submit response\",\n  result,\n  className,\n}: ApprovalCardProps) {\n  const reduce = useReducedMotion() ?? false;\n  const [internalAnswers, setInternalAnswers] =\n    useState<ApprovalCardAnswers>(defaultAnswers);\n  const [internalStep, setInternalStep] = useState(defaultStep);\n  const autoAdvanceTimer = useRef<number | undefined>(undefined);\n  const currentAnswers = answers ?? internalAnswers;\n  const currentStep = Math.min(\n    Math.max(0, step ?? internalStep),\n    Math.max(0, questions.length - 1),\n  );\n  const question = questions[currentStep];\n  const questionMode = questions.length > 0;\n  const pending = status === \"pending\";\n  const busy = status === \"submitting\";\n  const interactive = pending || busy;\n  const currentAnswer = question\n    ? (currentAnswers[question.id] ?? EMPTY_ANSWER)\n    : EMPTY_ANSWER;\n  const displayTitle = question?.title ?? title;\n  const titleKey = question?.id ?? String(status);\n  const statusLabel = getStatusLabel(status);\n\n  const clearAutoAdvance = useCallback(() => {\n    if (autoAdvanceTimer.current === undefined) return;\n    window.clearTimeout(autoAdvanceTimer.current);\n    autoAdvanceTimer.current = undefined;\n  }, []);\n\n  useEffect(() => clearAutoAdvance, [clearAutoAdvance]);\n\n  const setAnswers = useCallback(\n    (next: ApprovalCardAnswers) => {\n      if (answers === undefined) setInternalAnswers(next);\n      onAnswersChange?.(next);\n    },\n    [answers, onAnswersChange],\n  );\n\n  const setStep = (next: number) => {\n    clearAutoAdvance();\n    if (step === undefined) setInternalStep(next);\n    onStepChange?.(next);\n  };\n\n  const updateCurrentAnswer = (next: ApprovalCardAnswer) => {\n    if (!question) return;\n    setAnswers({ ...currentAnswers, [question.id]: next });\n  };\n\n  const continueQuestion = () => {\n    if (currentStep < questions.length - 1) {\n      setStep(currentStep + 1);\n      return;\n    }\n    onSubmit?.(currentAnswers);\n  };\n\n  const queueAutoAdvance = () => {\n    if (\n      !question ||\n      question.multiple ||\n      question.autoAdvance === false ||\n      currentStep >= questions.length - 1 ||\n      busy\n    ) {\n      return;\n    }\n\n    clearAutoAdvance();\n    autoAdvanceTimer.current = window.setTimeout(() => {\n      setStep(currentStep + 1);\n    }, 240);\n  };\n\n  return (\n    <div\n      data-state={status}\n      aria-busy={busy}\n      className={cn(\n        \"w-full overflow-hidden rounded-2xl bg-muted p-4 text-sm\",\n        className,\n      )}\n    >\n      <div className=\"flex items-start gap-3\">\n        <span\n          aria-hidden=\"true\"\n          className={cn(\n            \"grid size-5 shrink-0 place-items-center text-muted-foreground\",\n            getStatusClass(status),\n          )}\n        >\n          {busy ? (\n            <LoaderCircle className={cn(\"size-4\", !reduce && \"animate-spin\")} />\n          ) : interactive ? (\n            questionMode ? (\n              <CircleHelp className=\"size-4\" />\n            ) : (\n              <MessageSquareText className=\"size-4\" />\n            )\n          ) : status === \"rejected\" ? (\n            <X className=\"size-4\" />\n          ) : (\n            <Check className=\"size-4\" />\n          )}\n        </span>\n\n        <div className=\"min-w-0 flex-1\">\n          <div className=\"flex min-w-0 items-start gap-3\">\n            <h3 className=\"min-w-0 flex-1 text-base font-medium leading-5 text-foreground\">\n              <ActionSwapRollText value={titleKey}>\n                {displayTitle}\n              </ActionSwapRollText>\n            </h3>\n            {questionMode && interactive ? (\n              <span className=\"shrink-0 text-xs tabular-nums text-muted-foreground/65\">\n                {currentStep + 1}/{questions.length}\n              </span>\n            ) : (\n              <span\n                className={cn(\n                  \"shrink-0 rounded-full border px-2 py-0.5 text-[11px] font-medium transition-colors\",\n                  getStatusBadgeClass(status),\n                )}\n              >\n                {statusLabel}\n              </span>\n            )}\n            {onDismiss ? (\n              <button\n                type=\"button\"\n                aria-label=\"Dismiss\"\n                onClick={onDismiss}\n                className=\"grid size-5 shrink-0 place-items-center rounded-full text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n              >\n                <X className=\"size-4\" />\n              </button>\n            ) : null}\n          </div>\n\n          <AgentDisclosure open={interactive}>\n            {questionMode && question ? (\n              <AnimatePresence initial={false} mode=\"wait\">\n                <motion.div\n                  key={question.id}\n                  initial={reduce ? { opacity: 1 } : { opacity: 0, x: 8 }}\n                  animate={{ opacity: 1, x: 0 }}\n                  exit={reduce ? { opacity: 0 } : { opacity: 0, x: -6 }}\n                  transition={{ duration: reduce ? 0 : 0.2, ease: EASE_OUT }}\n                >\n                  {question.description ? (\n                    <p className=\"mt-1 leading-5 text-muted-foreground\">\n                      {question.description}\n                    </p>\n                  ) : null}\n                  <QuestionOptions\n                    question={question}\n                    answer={currentAnswer}\n                    disabled={busy}\n                    onChange={updateCurrentAnswer}\n                    onSingleSelect={queueAutoAdvance}\n                  />\n                </motion.div>\n              </AnimatePresence>\n            ) : (\n              <div>\n                {description ? (\n                  <p className=\"mt-1 leading-5 text-muted-foreground\">\n                    {description}\n                  </p>\n                ) : null}\n                {children ? <div className=\"mt-3\">{children}</div> : null}\n              </div>\n            )}\n\n            {questionMode ? (\n              <div className=\"mt-4 flex items-center gap-3\">\n                <Button\n                  variant=\"ghost\"\n                  size=\"icon\"\n                  aria-label=\"Previous question\"\n                  disabled={busy || currentStep === 0}\n                  onClick={() => setStep(currentStep - 1)}\n                  className=\"rounded-full\"\n                >\n                  <ArrowLeft className=\"size-4\" />\n                </Button>\n                <ProgressDots\n                  current={currentStep}\n                  ids={questions.map((item) => item.id)}\n                />\n                <Button\n                  size={currentStep === questions.length - 1 ? \"sm\" : \"icon\"}\n                  aria-label={\n                    currentStep === questions.length - 1\n                      ? \"Submit response\"\n                      : \"Next question\"\n                  }\n                  disabled={busy || !isAnswered(currentAnswer)}\n                  onClick={continueQuestion}\n                  className=\"ml-auto rounded-full\"\n                >\n                  {busy ? (\n                    <LoaderCircle className={cn(\"size-4\", !reduce && \"animate-spin\")} />\n                  ) : currentStep === questions.length - 1 ? (\n                    <>\n                      {submitLabel}\n                      <ArrowRight className=\"size-3.5\" />\n                    </>\n                  ) : (\n                    <ArrowRight className=\"size-4\" />\n                  )}\n                </Button>\n              </div>\n            ) : (\n              <div className=\"mt-4 flex flex-wrap items-center gap-2\">\n                <Button\n                  size=\"sm\"\n                  disabled={busy}\n                  onClick={onApprove}\n                  className=\"rounded-full\"\n                >\n                  {approveLabel}\n                </Button>\n                {onRequestChanges ? (\n                  <Button\n                    variant=\"secondary\"\n                    size=\"sm\"\n                    disabled={busy}\n                    onClick={onRequestChanges}\n                    className=\"rounded-full\"\n                  >\n                    Request changes\n                  </Button>\n                ) : null}\n                {onReject ? (\n                  <Button\n                    variant=\"ghost\"\n                    size=\"sm\"\n                    disabled={busy}\n                    onClick={onReject}\n                    className=\"rounded-full text-muted-foreground hover:text-rose-600 dark:hover:text-rose-400\"\n                  >\n                    Reject\n                  </Button>\n                ) : null}\n              </div>\n            )}\n          </AgentDisclosure>\n\n          {!interactive ? (\n            <p className=\"mt-1 text-sm text-muted-foreground\">\n              {result ?? statusLabel}\n            </p>\n          ) : null}\n        </div>\n      </div>\n    </div>\n  );\n}\n"},{"path":"components/agents/code-block.tsx","type":"registry:component","target":"@components/agents/code-block.tsx","content":"\"use client\";\n// beui.dev/components/agents/chat-app\n\nimport { Check, Copy, FileCode2, LoaderCircle } from \"lucide-react\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport {\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport {\n  type AgentCodeLanguage,\n  AgentCodeLine,\n  useAgentCodeTokens,\n} from \"@/components/agents/agent-code\";\nimport { SPRING_PRESS } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport type CodeBlockStatus = \"streaming\" | \"complete\";\n\nexport interface CodeBlockProps {\n  code: string;\n  language?: AgentCodeLanguage;\n  filename?: ReactNode;\n  status?: CodeBlockStatus;\n  showLineNumbers?: boolean;\n  highlightLines?: number[];\n  maxHeight?: number;\n  wrap?: boolean;\n  copyable?: boolean;\n  onCopy?: () => void | Promise<void>;\n  className?: string;\n}\n\nexport function CodeBlock({\n  code,\n  language = \"typescript\",\n  filename,\n  status = \"complete\",\n  showLineNumbers = true,\n  highlightLines = [],\n  maxHeight = 280,\n  wrap = false,\n  copyable = true,\n  onCopy,\n  className,\n}: CodeBlockProps) {\n  const reduce = useReducedMotion() ?? false;\n  const viewportRef = useRef<HTMLDivElement>(null);\n  const copyTimer = useRef<number | undefined>(undefined);\n  const [copied, setCopied] = useState(false);\n  const streaming = status === \"streaming\";\n  const tokens = useAgentCodeTokens(code, language);\n  const highlighted = useMemo(\n    () => new Set(highlightLines),\n    [highlightLines],\n  );\n  let offset = 0;\n  const lines = code.split(\"\\n\").map((content) => {\n    const line = { content, offset };\n    offset += content.length + 1;\n    return line;\n  });\n\n  useEffect(\n    () => () => {\n      if (copyTimer.current) window.clearTimeout(copyTimer.current);\n    },\n    [],\n  );\n\n  useLayoutEffect(() => {\n    const viewport = viewportRef.current;\n    if (!viewport || !streaming) return;\n\n    const frame = requestAnimationFrame(() => {\n      if (viewport.scrollHeight <= viewport.clientHeight) return;\n      if (typeof viewport.scrollTo === \"function\") {\n        viewport.scrollTo({\n          top: viewport.scrollHeight,\n          behavior: reduce ? \"auto\" : \"smooth\",\n        });\n      } else {\n        viewport.scrollTop = viewport.scrollHeight;\n      }\n    });\n    return () => cancelAnimationFrame(frame);\n  });\n\n  const handleCopy = useCallback(async () => {\n    if (onCopy) await onCopy();\n    else await navigator.clipboard?.writeText(code);\n\n    setCopied(true);\n    if (copyTimer.current) window.clearTimeout(copyTimer.current);\n    copyTimer.current = window.setTimeout(() => setCopied(false), 1600);\n  }, [code, onCopy]);\n\n  return (\n    <div\n      data-state={status}\n      aria-busy={streaming}\n      className={cn(\n        \"w-full overflow-hidden rounded-2xl bg-muted/80 text-sm\",\n        className,\n      )}\n    >\n      <div className=\"flex h-10 items-center gap-2.5 px-3\">\n        <FileCode2\n          aria-hidden=\"true\"\n          className=\"size-3.5 shrink-0 text-muted-foreground/70\"\n        />\n        {filename ? (\n          <span className=\"min-w-0 truncate font-mono text-xs text-foreground/80\">\n            {filename}\n          </span>\n        ) : null}\n        <span className=\"text-[10px] font-medium uppercase tracking-wide text-muted-foreground/55\">\n          {language}\n        </span>\n        <span\n          className={cn(\n            \"ml-auto inline-flex shrink-0 items-center gap-1 text-[10px] font-medium\",\n            streaming\n              ? \"text-blue-600 dark:text-blue-400\"\n              : \"text-emerald-600 dark:text-emerald-400\",\n          )}\n        >\n          {streaming ? (\n            <LoaderCircle className={cn(\"size-3\", !reduce && \"animate-spin\")} />\n          ) : (\n            <Check className=\"size-3\" />\n          )}\n          {streaming ? \"Writing\" : \"Ready\"}\n        </span>\n        {copyable || onCopy ? (\n          <motion.button\n            type=\"button\"\n            aria-label={copied ? \"Copied\" : \"Copy code\"}\n            title={copied ? \"Copied\" : \"Copy code\"}\n            onClick={handleCopy}\n            whileTap={reduce ? undefined : { scale: 0.9 }}\n            transition={SPRING_PRESS}\n            className=\"grid size-7 shrink-0 place-items-center rounded-full text-muted-foreground outline-none transition-colors hover:bg-background/70 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n          >\n            {copied ? (\n              <Check className=\"size-3.5\" />\n            ) : (\n              <Copy className=\"size-3.5\" />\n            )}\n          </motion.button>\n        ) : null}\n      </div>\n\n      <div\n        ref={viewportRef}\n        role={streaming ? \"log\" : undefined}\n        aria-live={streaming ? \"polite\" : undefined}\n        className=\"scrollbar-hide overflow-auto border-t border-foreground/[0.06] py-2\"\n        style={{ maxHeight }}\n      >\n        <pre className=\"m-0 min-w-max font-mono text-xs leading-5 text-foreground/85\">\n          <code>\n            {lines.map((line, index) => {\n              const lineNumber = index + 1;\n              return (\n                <span\n                  key={line.offset}\n                  className={cn(\n                    \"grid min-h-5\",\n                    showLineNumbers\n                      ? \"grid-cols-[2.75rem_minmax(0,1fr)]\"\n                      : \"grid-cols-1\",\n                    highlighted.has(lineNumber) && \"bg-blue-500/[0.07]\",\n                  )}\n                >\n                  {showLineNumbers ? (\n                    <span className=\"select-none pr-3 text-right tabular-nums text-muted-foreground/35\">\n                      {lineNumber}\n                    </span>\n                  ) : null}\n                  <AgentCodeLine\n                    code={line.content}\n                    tokens={tokens?.[index]}\n                    className={cn(\n                      \"pr-4\",\n                      showLineNumbers ? \"pl-1\" : \"pl-4\",\n                      wrap ? \"whitespace-pre-wrap break-words\" : \"whitespace-pre\",\n                    )}\n                  />\n                </span>\n              );\n            })}\n          </code>\n        </pre>\n      </div>\n    </div>\n  );\n}\n"},{"path":"components/agents/file-diff.tsx","type":"registry:component","target":"@components/agents/file-diff.tsx","content":"\"use client\";\n// beui.dev/components/agents/chat-app\n\nimport {\n  Check,\n  ChevronDown,\n  Copy,\n  FileCode2,\n  LoaderCircle,\n} from \"lucide-react\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport {\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport {\n  type AgentCodeLanguage,\n  AgentCodeLine,\n  useAgentCodeTokens,\n} from \"@/components/agents/agent-code\";\nimport { AgentDisclosure } from \"@/components/agents/agent-disclosure\";\nimport { SPRING_PRESS, SPRING_SWAP } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport type FileDiffStatus = \"streaming\" | \"complete\";\nexport type FileDiffLineType = \"added\" | \"removed\" | \"context\";\n\nexport interface FileDiffLine {\n  id: string;\n  type?: FileDiffLineType;\n  oldLine?: number;\n  newLine?: number;\n  content: string;\n}\n\nexport interface FileDiffProps {\n  file: ReactNode;\n  lines: FileDiffLine[];\n  status?: FileDiffStatus;\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  collapseOnComplete?: boolean;\n  maxHeight?: number;\n  language?: AgentCodeLanguage;\n  copyText?: string;\n  onCopy?: () => void | Promise<void>;\n  className?: string;\n}\n\nfunction ChangeCount({ value, type }: { value: number; type: \"added\" | \"removed\" }) {\n  if (!value) return null;\n  return (\n    <span\n      className={cn(\n        \"font-mono text-xs tabular-nums\",\n        type === \"added\"\n          ? \"text-emerald-600 dark:text-emerald-400\"\n          : \"text-rose-600 dark:text-rose-400\",\n      )}\n    >\n      {type === \"added\" ? \"+\" : \"−\"}\n      {value}\n    </span>\n  );\n}\n\nexport function FileDiff({\n  file,\n  lines,\n  status = \"streaming\",\n  open,\n  defaultOpen = true,\n  onOpenChange,\n  collapseOnComplete = true,\n  maxHeight = 220,\n  language = \"typescript\",\n  copyText,\n  onCopy,\n  className,\n}: FileDiffProps) {\n  const reduce = useReducedMotion() ?? false;\n  const baseId = useId();\n  const triggerId = `${baseId}-trigger`;\n  const contentId = `${baseId}-content`;\n  const viewportRef = useRef<HTMLDivElement>(null);\n  const previousStatus = useRef(status);\n  const copyTimer = useRef<number | undefined>(undefined);\n  const [copied, setCopied] = useState(false);\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const currentOpen = open ?? internalOpen;\n  const streaming = status === \"streaming\";\n  const additions = lines.filter((line) => line.type === \"added\").length;\n  const deletions = lines.filter((line) => line.type === \"removed\").length;\n  const canCopy = Boolean(copyText || onCopy);\n  const code = lines.map((line) => line.content).join(\"\\n\");\n  const tokens = useAgentCodeTokens(code, language);\n\n  const setOpen = useCallback(\n    (next: boolean) => {\n      if (open === undefined) setInternalOpen(next);\n      onOpenChange?.(next);\n    },\n    [onOpenChange, open],\n  );\n\n  useEffect(() => {\n    if (previousStatus.current !== \"streaming\" && status === \"streaming\") {\n      setOpen(true);\n    }\n    if (\n      previousStatus.current === \"streaming\" &&\n      status === \"complete\" &&\n      collapseOnComplete\n    ) {\n      setOpen(false);\n    }\n    previousStatus.current = status;\n  }, [collapseOnComplete, setOpen, status]);\n\n  useEffect(\n    () => () => {\n      if (copyTimer.current) window.clearTimeout(copyTimer.current);\n    },\n    [],\n  );\n\n  useLayoutEffect(() => {\n    const viewport = viewportRef.current;\n    if (!viewport || !currentOpen || !streaming) return;\n\n    const frame = requestAnimationFrame(() => {\n      if (viewport.scrollHeight <= viewport.clientHeight) return;\n      if (typeof viewport.scrollTo === \"function\") {\n        viewport.scrollTo({\n          top: viewport.scrollHeight,\n          behavior: reduce ? \"auto\" : \"smooth\",\n        });\n      } else {\n        viewport.scrollTop = viewport.scrollHeight;\n      }\n    });\n    return () => cancelAnimationFrame(frame);\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  return (\n    <div\n      data-state={status}\n      aria-busy={streaming}\n      className={cn(\"w-full text-sm\", className)}\n    >\n      <button\n        id={triggerId}\n        type=\"button\"\n        aria-expanded={currentOpen}\n        aria-controls={contentId}\n        onClick={() => setOpen(!currentOpen)}\n        className=\"group flex min-h-9 w-full items-center gap-2 rounded-md py-1 text-left outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\"\n      >\n        <FileCode2\n          aria-hidden=\"true\"\n          className=\"size-4 shrink-0 text-muted-foreground\"\n        />\n        <span className=\"min-w-0 flex-1 truncate font-mono text-xs text-foreground/80\">\n          {file}\n        </span>\n        <span className=\"flex shrink-0 items-center gap-2\">\n          <ChangeCount value={additions} type=\"added\" />\n          <ChangeCount value={deletions} type=\"removed\" />\n        </span>\n        <span className=\"grid size-4 shrink-0 place-items-center text-muted-foreground/60\">\n          {streaming ? (\n            <LoaderCircle\n              aria-label=\"Applying changes\"\n              className={cn(\"size-3.5\", !reduce && \"animate-spin\")}\n            />\n          ) : (\n            <Check aria-label=\"Changes applied\" className=\"size-3.5\" />\n          )}\n        </span>\n        <motion.span\n          aria-hidden=\"true\"\n          animate={{ rotate: currentOpen ? 180 : 0 }}\n          transition={reduce ? { duration: 0 } : SPRING_SWAP}\n          className=\"shrink-0 text-muted-foreground/45 transition-colors group-hover:text-muted-foreground\"\n        >\n          <ChevronDown className=\"size-3.5\" />\n        </motion.span>\n      </button>\n\n      <AgentDisclosure\n        id={contentId}\n        role=\"region\"\n        aria-labelledby={triggerId}\n        open={currentOpen}\n      >\n        <div className=\"pl-6 pt-1.5\">\n          <div className=\"overflow-hidden rounded-xl bg-muted/80\">\n            <div\n              ref={viewportRef}\n              data-slot=\"file-diff-viewport\"\n              aria-live=\"polite\"\n              className=\"scrollbar-hide overflow-auto\"\n              style={{ maxHeight }}\n            >\n              <div className=\"font-mono text-xs leading-5\">\n                <span className=\"sr-only\">File changes</span>\n                {lines.map((line, index) => {\n                  const type = line.type ?? \"context\";\n                  return (\n                    <div\n                      key={line.id}\n                      className={cn(\n                        \"grid grid-cols-[2.25rem_2.25rem_1rem_minmax(0,1fr)]\",\n                        type === \"added\" && \"bg-emerald-500/[0.07]\",\n                        type === \"removed\" && \"bg-rose-500/[0.07]\",\n                      )}\n                    >\n                      <span className=\"select-none pr-2 text-right tabular-nums text-muted-foreground/40\">\n                        {line.oldLine}\n                      </span>\n                      <span className=\"select-none pr-2 text-right tabular-nums text-muted-foreground/40\">\n                        {line.newLine}\n                      </span>\n                      <span\n                        className={cn(\n                          \"select-none text-center text-muted-foreground/45\",\n                          type === \"added\" &&\n                            \"text-emerald-600 dark:text-emerald-400\",\n                          type === \"removed\" &&\n                            \"text-rose-600 dark:text-rose-400\",\n                        )}\n                      >\n                        {type === \"added\"\n                          ? \"+\"\n                          : type === \"removed\"\n                            ? \"−\"\n                            : \"\"}\n                      </span>\n                      <AgentCodeLine\n                        code={line.content}\n                        tokens={tokens?.[index]}\n                        className=\"min-w-0 whitespace-pre px-1.5\"\n                      />\n                    </div>\n                  );\n                })}\n              </div>\n            </div>\n\n            {canCopy ? (\n              <div className=\"flex justify-end px-2 pb-1.5 pt-1\">\n                <motion.button\n                  type=\"button\"\n                  aria-label={copied ? \"Copied\" : \"Copy diff\"}\n                  title={copied ? \"Copied\" : \"Copy diff\"}\n                  onClick={handleCopy}\n                  whileTap={reduce ? undefined : { scale: 0.9 }}\n                  transition={SPRING_PRESS}\n                  className=\"grid size-7 place-items-center rounded-md text-muted-foreground outline-none transition-colors hover:bg-background/70 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n                >\n                  {copied ? (\n                    <Check className=\"size-3.5\" />\n                  ) : (\n                    <Copy className=\"size-3.5\" />\n                  )}\n                </motion.button>\n              </div>\n            ) : null}\n          </div>\n        </div>\n      </AgentDisclosure>\n    </div>\n  );\n}\n"},{"path":"components/agents/image-generation.tsx","type":"registry:component","target":"@components/agents/image-generation.tsx","content":"\"use client\";\n// beui.dev/components/agents/chat-app\n\nimport { Check, CircleAlert, RotateCcw } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport type { CSSProperties, ReactNode } from \"react\";\nimport { useEffect, useRef } from \"react\";\nimport { EASE_IN_OUT, EASE_OUT, SPRING_PRESS } from \"@/lib/ease\";\nimport { useHoverCapable } from \"@/lib/hooks/use-hover-capable\";\nimport { cn } from \"@/lib/utils\";\n\nexport type ImageGenerationStatus =\n  | \"queued\"\n  | \"generating\"\n  | \"refining\"\n  | \"complete\"\n  | \"error\";\n\nexport interface ImageGenerationProps {\n  /** The completed media. Pass an img, Next Image, canvas, video, or custom preview. */\n  children?: ReactNode;\n  status?: ImageGenerationStatus;\n  /** Accessible description. Defaults to a description derived from prompt. */\n  label?: string;\n  prompt?: string;\n  resolution?: string;\n  /** CSS aspect ratio reserved before generated media is available. */\n  aspectRatio?: CSSProperties[\"aspectRatio\"];\n  size?: \"compact\" | \"fluid\";\n  /** Lets the active dither cluster follow fine-pointer movement. */\n  interactive?: boolean;\n  statusText?: string;\n  showStatus?: boolean;\n  onRetry?: () => void;\n  className?: string;\n  mediaClassName?: string;\n  statusClassName?: string;\n}\n\nconst STATUS_TEXT: Record<ImageGenerationStatus, string> = {\n  queued: \"Waiting to generate\",\n  generating: \"Generating image\",\n  refining: \"Refining details\",\n  complete: \"Image ready\",\n  error: \"Generation failed\",\n};\n\nconst MEDIA_STATE: Record<\n  ImageGenerationStatus,\n  { filter: string; opacity: number; scale: number }\n> = {\n  queued: { filter: \"blur(4px) saturate(0.75)\", opacity: 0, scale: 1.02 },\n  generating: { filter: \"blur(3px) saturate(0.85)\", opacity: 0, scale: 1.015 },\n  refining: { filter: \"blur(1.5px) saturate(0.95)\", opacity: 0.62, scale: 1.005 },\n  complete: { filter: \"blur(0px) saturate(1)\", opacity: 1, scale: 1 },\n  error: { filter: \"blur(2px) saturate(0.5)\", opacity: 0.28, scale: 1 },\n};\n\nconst OVERLAY_OPACITY: Record<ImageGenerationStatus, number> = {\n  queued: 1,\n  generating: 1,\n  refining: 0.48,\n  complete: 0,\n  error: 0,\n};\n\nconst DOT_GAP = 10;\nconst TWO_PI = Math.PI * 2;\n\nfunction DitherMark({\n  status,\n  reduce,\n}: {\n  status: ImageGenerationStatus;\n  reduce: boolean;\n}) {\n  if (status === \"complete\") {\n    return <Check aria-hidden=\"true\" className=\"size-3.5\" />;\n  }\n\n  if (status === \"error\") {\n    return <CircleAlert aria-hidden=\"true\" className=\"size-3.5\" />;\n  }\n\n  return (\n    <motion.span\n      aria-hidden=\"true\"\n      animate={reduce ? undefined : { rotate: 360 }}\n      transition={{\n        duration: 2.4,\n        ease: EASE_IN_OUT,\n        repeat: Number.POSITIVE_INFINITY,\n      }}\n      className=\"grid size-3.5 grid-cols-2 place-items-center gap-0.5\"\n    >\n      <span className=\"size-1 rounded-[1px] bg-current\" />\n      <span className=\"size-1 rounded-[1px] bg-current opacity-55\" />\n      <span className=\"size-1 rounded-[1px] bg-current opacity-55\" />\n      <span className=\"size-1 rounded-[1px] bg-current\" />\n    </motion.span>\n  );\n}\n\nfunction DitherField({\n  interactive,\n  reduce,\n  status,\n}: {\n  interactive: boolean;\n  reduce: boolean;\n  status: ImageGenerationStatus;\n}) {\n  const canHover = useHoverCapable();\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    const context = canvas?.getContext(\"2d\");\n    if (!canvas || !context) return;\n\n    let frame = 0;\n    let width = 0;\n    let height = 0;\n    let dotColor = \"currentColor\";\n    const pointer = {\n      x: 0,\n      y: 0,\n      targetX: 0,\n      targetY: 0,\n      inside: false,\n    };\n    const pointerEnabled = interactive && canHover && !reduce;\n\n    const resize = () => {\n      const rect = canvas.getBoundingClientRect();\n      width = rect.width || canvas.clientWidth || 208;\n      height = rect.height || canvas.clientHeight || 208;\n      const dpr = Math.min(window.devicePixelRatio || 1, 2);\n\n      canvas.width = Math.round(width * dpr);\n      canvas.height = Math.round(height * dpr);\n      context.setTransform(dpr, 0, 0, dpr, 0, 0);\n      dotColor = window.getComputedStyle(canvas).color;\n      pointer.x = width / 2;\n      pointer.y = height / 2;\n      pointer.targetX = pointer.x;\n      pointer.targetY = pointer.y;\n    };\n\n    const draw = (time: number) => {\n      context.clearRect(0, 0, width, height);\n\n      if (!pointer.inside) {\n        pointer.targetX =\n          width / 2 + (reduce ? 0 : Math.sin(time / 1700) * width * 0.12);\n        pointer.targetY =\n          height / 2 + (reduce ? 0 : Math.cos(time / 2100) * height * 0.1);\n      }\n\n      const follow = reduce ? 1 : pointer.inside ? 0.16 : 0.045;\n      pointer.x += (pointer.targetX - pointer.x) * follow;\n      pointer.y += (pointer.targetY - pointer.y) * follow;\n\n      const radius = Math.min(width, height) * 0.38;\n      const columns = Math.ceil(width / DOT_GAP) + 1;\n      const rows = Math.ceil(height / DOT_GAP) + 1;\n      const offsetX = (width - (columns - 1) * DOT_GAP) / 2;\n      const offsetY = (height - (rows - 1) * DOT_GAP) / 2;\n\n      context.fillStyle = dotColor;\n\n      for (let row = 0; row < rows; row += 1) {\n        for (let column = 0; column < columns; column += 1) {\n          const anchorX = offsetX + column * DOT_GAP;\n          const anchorY = offsetY + row * DOT_GAP;\n          const deltaX = anchorX - pointer.x;\n          const deltaY = anchorY - pointer.y;\n          const distance = Math.hypot(deltaX, deltaY);\n          const proximity = Math.max(0, 1 - distance / radius);\n          const influence = proximity * proximity * (3 - 2 * proximity);\n          const displacement = influence * influence * 9;\n          const directionX = distance > 0 ? deltaX / distance : 0;\n          const directionY = distance > 0 ? deltaY / distance : 0;\n          const x = anchorX + directionX * displacement;\n          const y = anchorY + directionY * displacement;\n          const dotRadius = 0.65 + influence * 0.85;\n\n          context.globalAlpha = 0.17 + influence * 0.72;\n          context.beginPath();\n          context.arc(x, y, dotRadius, 0, TWO_PI);\n          context.fill();\n        }\n      }\n\n      context.globalAlpha = 1;\n      if (!reduce) frame = window.requestAnimationFrame(draw);\n    };\n\n    const handlePointerMove = (event: PointerEvent) => {\n      if (!pointerEnabled) return;\n      const rect = canvas.getBoundingClientRect();\n      pointer.inside = true;\n      pointer.targetX = event.clientX - rect.left;\n      pointer.targetY = event.clientY - rect.top;\n    };\n\n    const handlePointerLeave = () => {\n      pointer.inside = false;\n    };\n\n    const resizeObserver =\n      typeof ResizeObserver === \"undefined\"\n        ? null\n        : new ResizeObserver(resize);\n\n    resize();\n    resizeObserver?.observe(canvas);\n    canvas.addEventListener(\"pointermove\", handlePointerMove, { passive: true });\n    canvas.addEventListener(\"pointerleave\", handlePointerLeave);\n    draw(0);\n\n    return () => {\n      if (frame) window.cancelAnimationFrame(frame);\n      resizeObserver?.disconnect();\n      canvas.removeEventListener(\"pointermove\", handlePointerMove);\n      canvas.removeEventListener(\"pointerleave\", handlePointerLeave);\n    };\n  }, [canHover, interactive, reduce]);\n\n  return (\n    <motion.div\n      aria-hidden=\"true\"\n      initial={false}\n      animate={{ opacity: OVERLAY_OPACITY[status] }}\n      transition={{ duration: reduce ? 0 : 0.4, ease: EASE_OUT }}\n      className=\"absolute inset-0 overflow-hidden bg-muted\"\n    >\n      <canvas\n        ref={canvasRef}\n        className=\"absolute inset-0 size-full text-foreground\"\n      />\n    </motion.div>\n  );\n}\n\nexport function ImageGeneration({\n  children,\n  status = \"generating\",\n  label,\n  prompt,\n  resolution = \"1024 × 1024\",\n  aspectRatio = \"1 / 1\",\n  size = \"compact\",\n  interactive = true,\n  statusText,\n  showStatus = true,\n  onRetry,\n  className,\n  mediaClassName,\n  statusClassName,\n}: ImageGenerationProps) {\n  const reduce = useReducedMotion() ?? false;\n  const active =\n    status === \"queued\" || status === \"generating\" || status === \"refining\";\n  const mediaState = MEDIA_STATE[status];\n  const resolvedStatusText = statusText ?? STATUS_TEXT[status];\n  const resolvedLabel =\n    label ?? (prompt ? `${resolvedStatusText}: ${prompt}` : resolvedStatusText);\n\n  return (\n    <div\n      data-slot=\"image-generation\"\n      data-state={status}\n      aria-busy={active}\n      className={cn(\"w-full\", className)}\n    >\n      <div\n        className={cn(\n          \"w-full\",\n          size === \"compact\" && \"mx-auto max-w-52\",\n        )}\n      >\n        <div\n          role=\"img\"\n          aria-label={resolvedLabel}\n          style={{ aspectRatio }}\n          className=\"relative isolate w-full overflow-hidden rounded-xl bg-muted\"\n        >\n          <motion.div\n            aria-hidden={children ? undefined : true}\n            initial={false}\n            animate={\n              reduce\n                ? { opacity: mediaState.opacity }\n                : {\n                    filter: mediaState.filter,\n                    opacity: mediaState.opacity,\n                    scale: mediaState.scale,\n                  }\n            }\n            transition={\n              reduce ? { duration: 0 } : { duration: 0.4, ease: EASE_OUT }\n            }\n            className={cn(\n              \"absolute inset-0 [&>*]:size-full [&>*]:object-cover [&_img]:size-full [&_img]:object-cover\",\n              mediaClassName,\n            )}\n          >\n            {children}\n          </motion.div>\n\n          <AnimatePresence initial={false}>\n            {active ? (\n              <motion.div\n                key=\"dither-field\"\n                initial={{ opacity: 0 }}\n                animate={{ opacity: 1 }}\n                exit={{ opacity: 0 }}\n                transition={{ duration: reduce ? 0 : 0.25, ease: EASE_OUT }}\n                className=\"absolute inset-0\"\n              >\n                <DitherField\n                  interactive={interactive}\n                  reduce={reduce}\n                  status={status}\n                />\n              </motion.div>\n            ) : null}\n          </AnimatePresence>\n\n          {resolution ? (\n            <span className=\"absolute top-2 right-2 z-10 rounded-full bg-background/75 px-2 py-0.5 font-mono text-[10px] tabular-nums text-muted-foreground\">\n              {resolution}\n            </span>\n          ) : null}\n        </div>\n\n        {showStatus || prompt ? (\n          <div className=\"mt-3 text-left\">\n            {showStatus ? (\n              <div\n                aria-live=\"polite\"\n                className={cn(\n                  \"flex min-h-5 items-center gap-2 text-sm font-medium text-foreground\",\n                  status === \"error\" && \"text-destructive\",\n                  statusClassName,\n                )}\n              >\n                <DitherMark status={status} reduce={reduce} />\n                <AnimatePresence mode=\"popLayout\" initial={false}>\n                  <motion.span\n                    key={resolvedStatusText}\n                    initial={reduce ? false : { opacity: 0, y: 4 }}\n                    animate={{ opacity: 1, y: 0 }}\n                    exit={reduce ? undefined : { opacity: 0, y: -4 }}\n                    transition={{\n                      duration: reduce ? 0 : 0.15,\n                      ease: EASE_OUT,\n                    }}\n                  >\n                    {resolvedStatusText}\n                  </motion.span>\n                </AnimatePresence>\n              </div>\n            ) : null}\n            {prompt ? (\n              <p className=\"mt-0.5 truncate text-xs text-muted-foreground\">\n                “{prompt}”\n              </p>\n            ) : null}\n          </div>\n        ) : null}\n\n        {status === \"error\" && onRetry ? (\n          <motion.button\n            type=\"button\"\n            onClick={onRetry}\n            whileTap={reduce ? undefined : { scale: 0.96 }}\n            transition={SPRING_PRESS}\n            className=\"mt-3 inline-flex min-h-10 items-center gap-2 rounded-full px-3 text-sm font-medium text-foreground outline-none transition-colors hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring\"\n          >\n            <RotateCcw aria-hidden=\"true\" className=\"size-4\" />\n            Try again\n          </motion.button>\n        ) : null}\n      </div>\n    </div>\n  );\n}\n"},{"path":"components/agents/loading-states/thinking-shimmer.tsx","type":"registry:component","target":"@components/agents/loading-states/thinking-shimmer.tsx","content":"// beui.dev/components/agents/chat-app\nimport type { ReactNode } from \"react\";\nimport { TextShimmer } from \"@/components/motion/text-shimmer\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface ThinkingShimmerProps {\n  /** Loading message shown to the user. */\n  children?: ReactNode;\n  /** Seconds taken for one shimmer pass. */\n  duration?: number;\n  className?: string;\n}\n\nexport function ThinkingShimmer({\n  children = \"Thinking…\",\n  duration = 1.8,\n  className,\n}: ThinkingShimmerProps) {\n  return (\n    <TextShimmer\n      as=\"span\"\n      duration={duration}\n      className={cn(\"font-medium\", className)}\n    >\n      {children}\n    </TextShimmer>\n  );\n}\n"},{"path":"components/agents/message.tsx","type":"registry:component","target":"@components/agents/message.tsx","content":"\"use client\";\n// beui.dev/components/agents/chat-app\n\nimport { motion, useReducedMotion } from \"motion/react\";\nimport {\n  type ComponentPropsWithRef,\n  createContext,\n  type ReactNode,\n  useContext,\n} from \"react\";\nimport { EASE_OUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\nimport { MessageSideContext } from \"@/components/agents/message-context\";\n\nexport {\n  MessageBubble,\n  MessageBubbleCollapsible,\n  MessageBubbleContent,\n  MessageBubbleGroup,\n} from \"@/components/agents/message-bubble\";\nexport { MessageScroller } from \"@/components/agents/message-scroller\";\nexport type { MessageScrollerProps } from \"@/components/agents/message-scroller\";\n\nexport type MessageFrom = \"user\" | \"assistant\";\n\ninterface MessageContextValue {\n  from: MessageFrom;\n}\n\nconst MessageContext = createContext<MessageContextValue>({\n  from: \"assistant\",\n});\n\nexport interface MessageProps\n  extends Omit<ComponentPropsWithRef<typeof motion.article>, \"children\"> {\n  from: MessageFrom;\n  /** Plays a trailing-edge pop-up once when this message row mounts. */\n  animateIn?: boolean;\n  children: ReactNode;\n}\n\nexport interface MessageGroupProps extends ComponentPropsWithRef<\"div\"> {\n  spacing?: \"compact\" | \"default\";\n}\n\nexport interface MessageAvatarProps extends ComponentPropsWithRef<\"div\"> {\n  /** Keep an empty avatar slot so grouped messages remain aligned. */\n  placeholder?: boolean;\n}\n\nexport type MessageContentProps = ComponentPropsWithRef<\"div\">;\nexport type MessageHeaderProps = ComponentPropsWithRef<\"div\">;\nexport type MessageFooterProps = ComponentPropsWithRef<\"div\">;\n\nexport type MessageMarkerProps = ComponentPropsWithRef<\"div\">;\n\nexport interface MessageTypingProps extends ComponentPropsWithRef<\"span\"> {\n  label?: string;\n}\n\n// A sent row should rise from the live edge without changing measured layout.\nconst MESSAGE_POP_UP = {\n  type: \"spring\",\n  stiffness: 480,\n  damping: 32,\n  mass: 0.62,\n} as const;\n\nexport function Message({\n  from,\n  animateIn = false,\n  children,\n  className,\n  initial,\n  animate,\n  transition,\n  exit,\n  style,\n  ...props\n}: MessageProps) {\n  const reduce = useReducedMotion() ?? false;\n\n  return (\n    <MessageSideContext.Provider value={from === \"user\" ? \"end\" : \"start\"}>\n      <MessageContext.Provider value={{ from }}>\n        <motion.article\n          data-slot=\"message\"\n          data-from={from}\n          aria-label={props[\"aria-label\"] ?? `${from} message`}\n          initial={\n            initial ??\n            (animateIn && !reduce\n              ? {\n                  opacity: 0,\n                  transform: \"translateY(8px) scale(0.95)\",\n                }\n              : false)\n          }\n          animate={\n            animate ??\n            (animateIn && !reduce\n              ? {\n                  opacity: 1,\n                  transform: \"translateY(0px) scale(1)\",\n                }\n              : { opacity: 1 })\n          }\n          exit={\n            exit ??\n            (reduce\n              ? { opacity: 0 }\n              : {\n                  opacity: 0,\n                  transform: \"translateY(-3px) scale(0.99)\",\n                })\n          }\n          transition={\n            transition ?? (reduce ? { duration: 0.12 } : MESSAGE_POP_UP)\n          }\n          style={{\n            transformOrigin: from === \"user\" ? \"100% 100%\" : \"0% 100%\",\n            ...style,\n          }}\n          className={cn(\n            \"group/message flex w-full items-start gap-2\",\n            from === \"user\" ? \"flex-row-reverse\" : \"flex-row\",\n            className,\n          )}\n          {...props}\n        >\n          {children}\n        </motion.article>\n      </MessageContext.Provider>\n    </MessageSideContext.Provider>\n  );\n}\n\nexport function MessageGroup({\n  spacing = \"compact\",\n  className,\n  ...props\n}: MessageGroupProps) {\n  return (\n    <div\n      data-slot=\"message-group\"\n      className={cn(\n        \"flex w-full flex-col\",\n        spacing === \"compact\" ? \"gap-1.5\" : \"gap-4\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function MessageAvatar({\n  placeholder = false,\n  children,\n  className,\n  ...props\n}: MessageAvatarProps) {\n  return (\n    <div\n      data-slot=\"message-avatar\"\n      aria-hidden={placeholder || undefined}\n      className={cn(\n        \"grid size-7 shrink-0 place-items-center overflow-hidden rounded-full bg-muted text-xs font-medium text-muted-foreground [&_img]:size-full [&_img]:object-cover [&_svg]:size-3.5\",\n        placeholder && \"invisible\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function MessageContent({ className, ...props }: MessageContentProps) {\n  const { from } = useContext(MessageContext);\n\n  return (\n    <div\n      data-slot=\"message-content\"\n      className={cn(\n        \"flex min-w-0 flex-1 flex-col gap-1.5\",\n        from === \"user\" ? \"items-end\" : \"items-start\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function MessageHeader({ className, ...props }: MessageHeaderProps) {\n  const { from } = useContext(MessageContext);\n\n  return (\n    <div\n      data-slot=\"message-header\"\n      className={cn(\n        \"flex items-center gap-1.5 px-1 text-[11px] leading-none text-muted-foreground\",\n        from === \"user\" ? \"justify-end\" : \"justify-start\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function MessageFooter({ className, ...props }: MessageFooterProps) {\n  const { from } = useContext(MessageContext);\n\n  return (\n    <div\n      data-slot=\"message-footer\"\n      className={cn(\n        \"flex min-h-5 items-center gap-1 px-1 text-[11px] text-muted-foreground\",\n        from === \"user\" ? \"justify-end\" : \"justify-start\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function MessageMarker({ className, ...props }: MessageMarkerProps) {\n  return (\n    <div\n      data-slot=\"message-marker\"\n      className={cn(\n        \"mx-auto flex w-fit max-w-[88%] items-center gap-1.5 rounded-full bg-muted/70 px-2.5 py-1 text-center text-xs text-muted-foreground\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function MessageTyping({\n  label = \"Responding\",\n  className,\n  ...props\n}: MessageTypingProps) {\n  const reduce = useReducedMotion() ?? false;\n\n  return (\n    <span\n      data-slot=\"message-typing\"\n      className={cn(\"inline-flex h-5 items-center gap-1\", className)}\n      {...props}\n    >\n      <span className=\"sr-only\">{label}</span>\n      {[0, 1, 2].map((index) => (\n        <motion.span\n          key={index}\n          aria-hidden=\"true\"\n          className=\"size-1 rounded-full bg-current\"\n          animate={\n            reduce\n              ? { opacity: 0.45 }\n              : { opacity: [0.28, 0.85, 0.28], y: [0, -2, 0] }\n          }\n          transition={{\n            duration: 1.05,\n            ease: EASE_OUT,\n            repeat: Number.POSITIVE_INFINITY,\n            delay: index * 0.14,\n          }}\n        />\n      ))}\n    </span>\n  );\n}\n"},{"path":"components/agents/message-bubble.tsx","type":"registry:component","target":"@components/agents/message-bubble.tsx","content":"\"use client\";\n// beui.dev/components/agents/chat-app\n\nimport { ChevronDown } from \"lucide-react\";\nimport {\n  type HTMLMotionProps,\n  motion,\n  useReducedMotion,\n} from \"motion/react\";\nimport {\n  cloneElement,\n  type ComponentPropsWithRef,\n  createContext,\n  type ReactElement,\n  type ReactNode,\n  type Ref,\n  useCallback,\n  useContext,\n  useId,\n  useState,\n} from \"react\";\nimport {\n  EASE_OUT,\n  SPRING_LAYOUT,\n  SPRING_SWAP,\n} from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\nimport { MessageSideContext } from \"@/components/agents/message-context\";\n\nexport type MessageBubbleVariant =\n  | \"solid\"\n  | \"soft\"\n  | \"tint\"\n  | \"outline\"\n  | \"ghost\"\n  | \"danger\";\nexport type MessageBubbleAlign = \"start\" | \"end\";\n\ninterface MessageBubbleContextValue {\n  align?: MessageBubbleAlign;\n  animateIn: boolean;\n  variant: MessageBubbleVariant;\n}\n\nconst MessageBubbleContext = createContext<MessageBubbleContextValue>({\n  animateIn: true,\n  variant: \"soft\",\n});\nconst MessageBubbleLayoutContext = createContext<() => void>(() => {});\n\nexport interface MessageBubbleProps\n  extends Omit<HTMLMotionProps<\"div\">, \"children\"> {\n  variant?: MessageBubbleVariant;\n  /** Defaults to the surrounding Message alignment when omitted. */\n  align?: MessageBubbleAlign;\n  /** Plays the bubble entrance once when this component mounts. */\n  animateIn?: boolean;\n  children?: ReactNode;\n}\n\nexport interface MessageBubbleContentProps\n  extends ComponentPropsWithRef<\"div\"> {\n  /** Replaces the content element while preserving bubble styling. */\n  render?: ReactElement;\n}\n\nexport interface MessageBubbleGroupProps extends ComponentPropsWithRef<\"div\"> {\n  spacing?: \"compact\" | \"default\";\n}\n\nexport interface MessageBubbleCollapsibleProps\n  extends ComponentPropsWithRef<\"div\"> {\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  collapsedLines?: 2 | 3 | 4 | 5 | 6;\n  moreLabel?: ReactNode;\n  lessLabel?: ReactNode;\n  contentClassName?: string;\n  triggerClassName?: string;\n  children?: ReactNode;\n}\n\nfunction mergeRefs<T>(...refs: Array<Ref<T> | undefined>) {\n  return (node: T | null) => {\n    for (const ref of refs) {\n      if (typeof ref === \"function\") ref(node);\n      else if (ref) ref.current = node;\n    }\n  };\n}\n\nconst BUBBLE_CONTENT_REVEAL = {\n  duration: 0.12,\n  ease: EASE_OUT,\n  delay: 0.04,\n} as const;\n\n// Sent bubbles should pop into place quickly with one restrained overshoot.\nconst BUBBLE_POP = {\n  type: \"spring\",\n  stiffness: 520,\n  damping: 27,\n  mass: 0.52,\n} as const;\n\nexport function MessageBubble({\n  variant = \"soft\",\n  align,\n  animateIn = false,\n  className,\n  children,\n  initial,\n  animate,\n  exit,\n  transition,\n  layout,\n  ...props\n}: MessageBubbleProps) {\n  const reduce = useReducedMotion() ?? false;\n  const messageSide = useContext(MessageSideContext);\n  const resolvedAlign = align ?? messageSide ?? \"start\";\n\n  return (\n    <MessageBubbleContext.Provider\n      value={{ align: resolvedAlign, animateIn, variant }}\n    >\n      <motion.div\n        data-slot=\"message-bubble\"\n        data-align={resolvedAlign}\n        data-variant={variant}\n        layout={layout}\n        initial={initial ?? false}\n        animate={animate}\n        exit={\n          exit ??\n          (reduce ? { opacity: 0 } : { opacity: 0, y: -3, scale: 0.99 })\n        }\n        transition={transition ?? (reduce ? { duration: 0.12 } : SPRING_LAYOUT)}\n        className={cn(\n          \"group/bubble flex w-full flex-col\",\n          resolvedAlign === \"end\" ? \"items-end\" : \"items-start\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n      </motion.div>\n    </MessageBubbleContext.Provider>\n  );\n}\n\nfunction bubbleContentClass(\n  variant: MessageBubbleVariant,\n  interactive: boolean,\n) {\n  return cn(\n    \"relative z-0 min-w-9 max-w-[82%] rounded-2xl px-3.5 py-2.5 text-sm leading-6 text-foreground\",\n    \"[&_a]:font-medium [&_a]:underline [&_a]:underline-offset-4 [&_code]:rounded [&_code]:bg-background/60 [&_code]:px-1 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-[0.9em] [&_ol]:my-2 [&_ol]:list-decimal [&_ol]:pl-5 [&_p+p]:mt-2 [&_pre]:my-2 [&_pre]:overflow-x-auto [&_pre]:rounded-xl [&_pre]:bg-background/60 [&_pre]:p-3 [&_ul]:my-2 [&_ul]:list-disc [&_ul]:pl-5\",\n    variant === \"solid\" && \"text-background\",\n    variant === \"ghost\" && \"w-full max-w-none rounded-none px-0 py-0\",\n    variant === \"danger\" && \"text-destructive\",\n    interactive &&\n      \"cursor-pointer text-left outline-none transition-[background-color,color,transform] duration-150 hover:brightness-[0.98] focus-visible:ring-2 focus-visible:ring-ring active:scale-[0.99]\",\n  );\n}\n\nfunction bubbleSurfaceClass(\n  variant: MessageBubbleVariant,\n  align: MessageBubbleAlign,\n) {\n  return cn(\n    \"pointer-events-none absolute inset-0 -z-10 rounded-[inherit]\",\n    align === \"end\" ? \"origin-bottom-right\" : \"origin-bottom-left\",\n    variant === \"solid\" && \"bg-foreground\",\n    variant === \"soft\" && \"bg-muted\",\n    variant === \"tint\" && \"bg-primary/10\",\n    variant === \"outline\" && \"border border-border/70 bg-background\",\n    variant === \"danger\" && \"bg-destructive/10\",\n  );\n}\n\nexport function MessageBubbleContent({\n  render,\n  className,\n  children,\n  ref,\n  ...props\n}: MessageBubbleContentProps) {\n  const reduce = useReducedMotion() ?? false;\n  const { align = \"start\", animateIn, variant } =\n    useContext(MessageBubbleContext);\n  const [layoutVersion, setLayoutVersion] = useState(0);\n  const notifyLayout = useCallback(\n    () => setLayoutVersion((version) => version + 1),\n    [],\n  );\n  const interactive =\n    render?.type === \"button\" || render?.type === \"a\";\n  const classes = cn(bubbleContentClass(variant, interactive), className);\n  const composedChildren = (\n    <>\n      {variant !== \"ghost\" ? (\n        <motion.span\n          aria-hidden=\"true\"\n          layout={reduce ? false : \"size\"}\n          layoutDependency={layoutVersion}\n          initial={\n            animateIn && !reduce\n              ? {\n                  opacity: 0,\n                  scale: 0.92,\n                }\n              : false\n          }\n          animate={{ opacity: 1, scale: 1 }}\n          transition={\n            reduce\n              ? { duration: 0 }\n              : {\n                  opacity: { duration: 0.12, ease: EASE_OUT },\n                  scale: BUBBLE_POP,\n                  layout: SPRING_LAYOUT,\n                }\n          }\n          className={bubbleSurfaceClass(variant, align)}\n        />\n      ) : null}\n      <MessageBubbleLayoutContext.Provider value={notifyLayout}>\n        <motion.div\n          initial={\n            animateIn\n              ? reduce\n                ? { opacity: 0 }\n                : { opacity: 0 }\n              : false\n          }\n          animate={{ opacity: 1 }}\n          transition={\n            reduce ? { duration: 0.12, ease: EASE_OUT } : BUBBLE_CONTENT_REVEAL\n          }\n          className=\"relative\"\n        >\n          {children}\n        </motion.div>\n      </MessageBubbleLayoutContext.Provider>\n    </>\n  );\n\n  if (render) {\n    const child = render as ReactElement<\n      Record<string, unknown> & { className?: string; ref?: Ref<HTMLElement> }\n    >;\n\n    return cloneElement(child, {\n      ...props,\n      ref: mergeRefs(child.props.ref, ref as Ref<HTMLElement> | undefined),\n      className: cn(classes, child.props.className),\n      children: composedChildren,\n      \"data-slot\": \"message-bubble-content\",\n    });\n  }\n\n  return (\n    <div\n      ref={ref}\n      data-slot=\"message-bubble-content\"\n      className={classes}\n      {...props}\n    >\n      {composedChildren}\n    </div>\n  );\n}\n\nexport function MessageBubbleGroup({\n  spacing = \"compact\",\n  className,\n  ...props\n}: MessageBubbleGroupProps) {\n  return (\n    <div\n      data-slot=\"message-bubble-group\"\n      className={cn(\n        \"flex w-full flex-col\",\n        spacing === \"compact\" ? \"gap-1.5\" : \"gap-3\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nconst LINE_CLAMP_CLASS = {\n  2: \"line-clamp-2\",\n  3: \"line-clamp-3\",\n  4: \"line-clamp-4\",\n  5: \"line-clamp-5\",\n  6: \"line-clamp-6\",\n} as const;\n\nexport function MessageBubbleCollapsible({\n  open,\n  defaultOpen = false,\n  onOpenChange,\n  collapsedLines = 4,\n  moreLabel = \"Show more\",\n  lessLabel = \"Show less\",\n  contentClassName,\n  triggerClassName,\n  className,\n  children,\n  ...props\n}: MessageBubbleCollapsibleProps) {\n  const reduce = useReducedMotion() ?? false;\n  const contentId = useId();\n  const notifyLayout = useContext(MessageBubbleLayoutContext);\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const currentOpen = open ?? internalOpen;\n\n  const setOpen = useCallback(\n    (next: boolean) => {\n      notifyLayout();\n      if (open === undefined) setInternalOpen(next);\n      onOpenChange?.(next);\n    },\n    [notifyLayout, onOpenChange, open],\n  );\n\n  return (\n    <div\n      data-slot=\"message-bubble-collapsible\"\n      data-state={currentOpen ? \"open\" : \"closed\"}\n      className={cn(\"w-full\", className)}\n      {...props}\n    >\n      <div\n        id={contentId}\n        className={cn(\n          \"transition-[mask-image] duration-200\",\n          !currentOpen && LINE_CLAMP_CLASS[collapsedLines],\n          !currentOpen &&\n            \"[mask-image:linear-gradient(to_bottom,#000_68%,transparent_100%)]\",\n          contentClassName,\n        )}\n      >\n        {children}\n      </div>\n      <button\n        type=\"button\"\n        aria-expanded={currentOpen}\n        aria-controls={contentId}\n        onClick={() => setOpen(!currentOpen)}\n        className={cn(\n          \"mt-2 inline-flex h-7 items-center gap-1 rounded-full px-2 text-xs font-medium text-muted-foreground outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\",\n          triggerClassName,\n        )}\n      >\n        <span>{currentOpen ? lessLabel : moreLabel}</span>\n        <motion.span\n          aria-hidden=\"true\"\n          animate={{ rotate: currentOpen ? 180 : 0 }}\n          transition={reduce ? { duration: 0 } : SPRING_SWAP}\n        >\n          <ChevronDown className=\"size-3.5\" />\n        </motion.span>\n      </button>\n    </div>\n  );\n}\n"},{"path":"components/agents/message-scroller.tsx","type":"registry:component","target":"@components/agents/message-scroller.tsx","content":"\"use client\";\n// beui.dev/components/agents/chat-app\n\nimport { useReducedMotion } from \"motion/react\";\nimport {\n  type ComponentPropsWithRef,\n  type Ref,\n  useCallback,\n  useEffect,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport {\n  PreviewRail,\n  type PreviewRailItem,\n} from \"@/components/motion/preview-rail\";\nimport { cn } from \"@/lib/utils\";\n\nconst PREVIEW_TITLE_LENGTH = 56;\nconst PREVIEW_DESCRIPTION_LENGTH = 88;\n\nfunction truncateMessageText(text: string, limit: number) {\n  if (text.length <= limit) return text;\n  const excerpt = text.slice(0, limit);\n  const boundary = excerpt.lastIndexOf(\" \");\n  return `${excerpt.slice(0, boundary > limit * 0.65 ? boundary : limit).trim()}…`;\n}\n\nfunction getMessageText(message: HTMLElement) {\n  const surface =\n    message.querySelector<HTMLElement>('[data-slot=\"message-bubble-content\"]') ??\n    message.querySelector<HTMLElement>('[data-slot=\"message-content\"]') ??\n    message;\n  return (surface.textContent ?? \"\").replace(/\\s+/g, \" \").trim();\n}\n\nfunction getMessagePreview(\n  message: HTMLElement,\n  assistantResponse?: HTMLElement,\n) {\n  const text = getMessageText(message);\n  if (!text) {\n    return { label: \"Message\", description: undefined };\n  }\n\n  if (text.length <= PREVIEW_TITLE_LENGTH) {\n    const responseText = assistantResponse\n      ? getMessageText(assistantResponse)\n      : \"\";\n    return {\n      label: text,\n      description: responseText\n        ? truncateMessageText(responseText, PREVIEW_DESCRIPTION_LENGTH)\n        : undefined,\n    };\n  }\n\n  const titleExcerpt = text.slice(0, PREVIEW_TITLE_LENGTH);\n  const titleBoundary = titleExcerpt.lastIndexOf(\" \");\n  const titleEnd =\n    titleBoundary > PREVIEW_TITLE_LENGTH * 0.65\n      ? titleBoundary\n      : PREVIEW_TITLE_LENGTH;\n  const label = `${text.slice(0, titleEnd).trim()}…`;\n  const responseText = assistantResponse\n    ? getMessageText(assistantResponse)\n    : text.slice(titleEnd).trim();\n  return {\n    label,\n    description: responseText\n      ? truncateMessageText(responseText, PREVIEW_DESCRIPTION_LENGTH)\n      : undefined,\n  };\n}\n\nexport interface MessageScrollerProps extends ComponentPropsWithRef<\"div\"> {\n  /** Keep streamed output pinned while the reader remains near the end. */\n  followOutput?: boolean;\n  /** Distance from the end that still counts as following the output. */\n  followThreshold?: number;\n  /** Smoothly follow growing content. */\n  smooth?: boolean;\n  /** Reports when the reader leaves or returns to the live edge. */\n  onFollowChange?: (following: boolean) => void;\n  /** Accessible label for the scrollable transcript. */\n  label?: string;\n  /** Marks the transcript as waiting for more streamed content. */\n  busy?: boolean;\n  /** Adds a compact rail for navigating between rendered Message rows. */\n  navigation?: \"rail\";\n  /** Accessible label for the optional message navigation rail. */\n  navigationLabel?: string;\n  viewportClassName?: string;\n  contentClassName?: string;\n  railClassName?: string;\n  viewportRef?: Ref<HTMLElement>;\n  viewportProps?: Omit<\n    ComponentPropsWithRef<\"section\">,\n    \"children\" | \"className\" | \"ref\"\n  >;\n  contentProps?: Omit<\n    ComponentPropsWithRef<\"div\">,\n    \"children\" | \"className\" | \"ref\"\n  >;\n}\n\nexport function MessageScroller({\n  followOutput = true,\n  followThreshold = 56,\n  smooth = true,\n  onFollowChange,\n  label = \"Conversation\",\n  busy,\n  navigation,\n  navigationLabel = \"Message navigation\",\n  viewportClassName,\n  contentClassName,\n  railClassName,\n  viewportRef: externalViewportRef,\n  viewportProps,\n  contentProps,\n  className,\n  children,\n  ...props\n}: MessageScrollerProps) {\n  const reduce = useReducedMotion() ?? false;\n  const viewportRef = useRef<HTMLElement>(null);\n  const contentRef = useRef<HTMLDivElement>(null);\n  const followingRef = useRef(followOutput);\n  const programmaticScrollRef = useRef(false);\n  const scrollTimerRef = useRef<number | undefined>(undefined);\n  const frameRef = useRef<number | undefined>(undefined);\n  const railFrameRef = useRef<number | undefined>(undefined);\n  const railIdRef = useRef(new WeakMap<HTMLElement, string>());\n  const railIdCounterRef = useRef(0);\n  const railTargetsRef = useRef(new Map<string, HTMLElement>());\n  const [railItems, setRailItems] = useState<PreviewRailItem[]>([]);\n  const [activeRailId, setActiveRailId] = useState(\"\");\n  const [railOverflowing, setRailOverflowing] = useState(false);\n  const {\n    onScroll: onViewportScroll,\n    onWheel: onViewportWheel,\n    onTouchStart: onViewportTouchStart,\n    onKeyDown: onViewportKeyDown,\n    ...restViewportProps\n  } = viewportProps ?? {};\n\n  const setViewportRef = useCallback(\n    (node: HTMLElement | null) => {\n      viewportRef.current = node;\n      if (typeof externalViewportRef === \"function\") {\n        externalViewportRef(node);\n      } else if (externalViewportRef) {\n        externalViewportRef.current = node;\n      }\n    },\n    [externalViewportRef],\n  );\n\n  const setFollowing = useCallback(\n    (next: boolean) => {\n      if (followingRef.current === next) return;\n      followingRef.current = next;\n      onFollowChange?.(next);\n    },\n    [onFollowChange],\n  );\n\n  const updateActiveRailItem = useCallback(() => {\n    if (navigation !== \"rail\") return;\n    const viewport = viewportRef.current;\n    const targets = [...railTargetsRef.current.entries()];\n    if (!viewport || targets.length === 0) return;\n\n    const viewportRect = viewport.getBoundingClientRect();\n    if (viewport.scrollTop <= followThreshold) {\n      const firstId = targets[0]?.[0] ?? \"\";\n      setActiveRailId((current) => (current === firstId ? current : firstId));\n      return;\n    }\n\n    const distanceFromEnd =\n      viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight;\n    if (distanceFromEnd <= followThreshold) {\n      const lastId = targets.at(-1)?.[0] ?? \"\";\n      setActiveRailId((current) => (current === lastId ? current : lastId));\n      return;\n    }\n\n    const viewportCenter = viewportRect.top + viewportRect.height / 2;\n    let nearestId = targets[0]?.[0] ?? \"\";\n    let nearestDistance = Number.POSITIVE_INFINITY;\n\n    for (const [id, element] of targets) {\n      const rect = element.getBoundingClientRect();\n      const messageCenter = rect.top + rect.height / 2;\n      const distance = Math.abs(messageCenter - viewportCenter);\n      if (distance < nearestDistance) {\n        nearestDistance = distance;\n        nearestId = id;\n      }\n    }\n\n    setActiveRailId((current) =>\n      current === nearestId ? current : nearestId,\n    );\n  }, [followThreshold, navigation]);\n\n  const syncRailItems = useCallback(() => {\n    if (navigation !== \"rail\") return;\n    const content = contentRef.current;\n    const viewport = viewportRef.current;\n    if (!content || !viewport) return;\n\n    const messages = Array.from(\n      content.querySelectorAll<HTMLElement>('[data-slot=\"message\"]'),\n    );\n    const targets = new Map<string, HTMLElement>();\n    const nextItems = messages.map((message, index) => {\n      let id = railIdRef.current.get(message);\n      if (!id) {\n        railIdCounterRef.current += 1;\n        id = `message-rail-${railIdCounterRef.current}`;\n        railIdRef.current.set(message, id);\n      }\n      targets.set(id, message);\n      const sender = message.dataset.from ?? \"conversation\";\n      const assistantResponse =\n        sender === \"user\"\n          ? messages\n              .slice(index + 1)\n              .find((candidate) => candidate.dataset.from === \"assistant\")\n          : undefined;\n      const preview = getMessagePreview(message, assistantResponse);\n\n      return {\n        id,\n        label: preview.label,\n        description: preview.description,\n        ariaLabel: `Go to ${sender} message ${index + 1} of ${messages.length}`,\n      };\n    });\n\n    railTargetsRef.current = targets;\n    setRailItems((current) => {\n      const unchanged =\n        current.length === nextItems.length &&\n        current.every(\n          (item, index) =>\n            item.id === nextItems[index]?.id &&\n            item.label === nextItems[index]?.label &&\n            item.description === nextItems[index]?.description &&\n            item.ariaLabel === nextItems[index]?.ariaLabel,\n        );\n      return unchanged ? current : nextItems;\n    });\n    setRailOverflowing(\n      viewport.scrollHeight > viewport.clientHeight + 1 && messages.length > 1,\n    );\n  }, [navigation]);\n\n  const scheduleRailSync = useCallback(() => {\n    if (navigation !== \"rail\") return;\n    if (railFrameRef.current) cancelAnimationFrame(railFrameRef.current);\n    railFrameRef.current = requestAnimationFrame(() => {\n      syncRailItems();\n      updateActiveRailItem();\n    });\n  }, [navigation, syncRailItems, updateActiveRailItem]);\n\n  const scrollToEnd = useCallback((behavior: ScrollBehavior) => {\n    const viewport = viewportRef.current;\n    if (!viewport) return;\n\n    programmaticScrollRef.current = true;\n    if (typeof viewport.scrollTo === \"function\") {\n      viewport.scrollTo({ top: viewport.scrollHeight, behavior });\n    } else {\n      viewport.scrollTop = viewport.scrollHeight;\n    }\n    if (scrollTimerRef.current) window.clearTimeout(scrollTimerRef.current);\n    scrollTimerRef.current = window.setTimeout(() => {\n      programmaticScrollRef.current = false;\n    }, behavior === \"smooth\" ? 320 : 0);\n  }, []);\n\n  const handleScroll = useCallback(() => {\n    const viewport = viewportRef.current;\n    if (!viewport || programmaticScrollRef.current) return;\n\n    const distance =\n      viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight;\n    setFollowing(distance <= followThreshold);\n    updateActiveRailItem();\n  }, [followThreshold, setFollowing, updateActiveRailItem]);\n\n  const leaveLiveEdge = useCallback(() => {\n    programmaticScrollRef.current = false;\n  }, []);\n\n  useLayoutEffect(() => {\n    followingRef.current = followOutput;\n    if (!followOutput) return;\n\n    frameRef.current = requestAnimationFrame(() => scrollToEnd(\"auto\"));\n    return () => {\n      if (frameRef.current) cancelAnimationFrame(frameRef.current);\n    };\n  }, [followOutput, scrollToEnd]);\n\n  useEffect(() => {\n    const content = contentRef.current;\n    if (!content || typeof ResizeObserver === \"undefined\") return;\n\n    const observer = new ResizeObserver(() => {\n      scheduleRailSync();\n      if (!followOutput || !followingRef.current) return;\n      scrollToEnd(reduce || !smooth ? \"auto\" : \"smooth\");\n    });\n    observer.observe(content);\n\n    return () => observer.disconnect();\n  }, [followOutput, reduce, scheduleRailSync, scrollToEnd, smooth]);\n\n  useEffect(() => {\n    if (navigation !== \"rail\") {\n      railTargetsRef.current.clear();\n      setRailItems([]);\n      setRailOverflowing(false);\n      return;\n    }\n\n    const content = contentRef.current;\n    const viewport = viewportRef.current;\n    if (!content || !viewport) return;\n\n    scheduleRailSync();\n    const mutationObserver =\n      typeof MutationObserver === \"undefined\"\n        ? null\n        : new MutationObserver(scheduleRailSync);\n    mutationObserver?.observe(content, {\n      childList: true,\n      characterData: true,\n      subtree: true,\n    });\n\n    const resizeObserver =\n      typeof ResizeObserver === \"undefined\"\n        ? null\n        : new ResizeObserver(scheduleRailSync);\n    resizeObserver?.observe(content);\n    resizeObserver?.observe(viewport);\n\n    return () => {\n      mutationObserver?.disconnect();\n      resizeObserver?.disconnect();\n    };\n  }, [navigation, scheduleRailSync]);\n\n  useEffect(\n    () => () => {\n      if (scrollTimerRef.current) window.clearTimeout(scrollTimerRef.current);\n      if (frameRef.current) cancelAnimationFrame(frameRef.current);\n      if (railFrameRef.current) cancelAnimationFrame(railFrameRef.current);\n    },\n    [],\n  );\n\n  const scrollToRailItem = useCallback(\n    (item: PreviewRailItem) => {\n      const viewport = viewportRef.current;\n      const target = railTargetsRef.current.get(item.id);\n      if (!viewport || !target) return;\n\n      const lastItem = railItems.at(-1)?.id === item.id;\n      setActiveRailId(item.id);\n      if (lastItem) {\n        setFollowing(true);\n        scrollToEnd(reduce || !smooth ? \"auto\" : \"smooth\");\n        return;\n      }\n\n      setFollowing(false);\n      programmaticScrollRef.current = true;\n      const viewportRect = viewport.getBoundingClientRect();\n      const targetRect = target.getBoundingClientRect();\n      const top =\n        viewport.scrollTop +\n        targetRect.top -\n        viewportRect.top -\n        (viewport.clientHeight - targetRect.height) / 2;\n      const behavior = reduce || !smooth ? \"auto\" : \"smooth\";\n\n      if (typeof viewport.scrollTo === \"function\") {\n        viewport.scrollTo({ top, behavior });\n      } else {\n        viewport.scrollTop = top;\n      }\n      if (scrollTimerRef.current) window.clearTimeout(scrollTimerRef.current);\n      scrollTimerRef.current = window.setTimeout(() => {\n        programmaticScrollRef.current = false;\n      }, behavior === \"smooth\" ? 320 : 0);\n    },\n    [railItems, reduce, scrollToEnd, setFollowing, smooth],\n  );\n\n  const viewport = (\n    <section\n      ref={setViewportRef}\n      aria-label={label}\n      {...restViewportProps}\n      onScroll={(event) => {\n        handleScroll();\n        onViewportScroll?.(event);\n      }}\n      onWheel={(event) => {\n        leaveLiveEdge();\n        onViewportWheel?.(event);\n      }}\n      onTouchStart={(event) => {\n        leaveLiveEdge();\n        onViewportTouchStart?.(event);\n      }}\n      onKeyDown={(event) => {\n        if ([\"ArrowUp\", \"PageUp\", \"Home\"].includes(event.key)) {\n          leaveLiveEdge();\n        }\n        onViewportKeyDown?.(event);\n      }}\n      className={cn(\n        \"h-full overflow-y-auto overscroll-contain outline-none [overflow-anchor:none] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring\",\n        navigation === \"rail\"\n          ? \"[-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\"\n          : \"[scrollbar-gutter:stable]\",\n        viewportClassName,\n        navigation === \"rail\" && railOverflowing && \"pr-10\",\n      )}\n    >\n      <div\n        ref={contentRef}\n        role=\"log\"\n        aria-live=\"polite\"\n        aria-relevant=\"additions text\"\n        aria-busy={busy}\n        className={contentClassName}\n        {...contentProps}\n      >\n        {children}\n      </div>\n    </section>\n  );\n\n  return (\n    <div\n      data-slot=\"message-scroller\"\n      className={cn(\"min-h-0\", className)}\n      {...props}\n    >\n      {navigation === \"rail\" ? (\n        <PreviewRail\n          items={railOverflowing ? railItems : []}\n          label={navigationLabel}\n          activeId={activeRailId}\n          onItemSelect={scrollToRailItem}\n          previewSide=\"before\"\n          highlightActive\n          itemSize={14}\n          className=\"h-full min-h-0 overflow-hidden\"\n          previewContainerClassName=\"right-8 left-3\"\n          previewClassName=\"mr-1 w-64 max-w-full [&_[data-slot=preview-rail-card]]:h-20 [&_[data-slot=preview-rail-card]]:overflow-hidden [&_[data-slot=preview-rail-card]]:p-3 [&_[data-slot=preview-rail-title]]:line-clamp-1 [&_[data-slot=preview-rail-title]]:text-xs [&_[data-slot=preview-rail-title]]:leading-4 [&_[data-slot=preview-rail-description]]:line-clamp-2 [&_[data-slot=preview-rail-description]]:text-xs [&_[data-slot=preview-rail-description]]:leading-4\"\n          railClassName={cn(\n            \"absolute inset-y-3 right-1 w-7 content-center py-1 [&_[data-slot=preview-rail-item]]:w-7 [&_[data-slot=preview-rail-item]]:justify-end [&_[data-slot=preview-rail-tick]]:h-px [&_[data-slot=preview-rail-tick]]:w-4 [&_[data-slot=preview-rail-tick]]:origin-right\",\n            railOverflowing\n              ? \"pointer-events-auto opacity-100\"\n              : \"pointer-events-none opacity-0\",\n            railClassName,\n          )}\n        >\n          {viewport}\n        </PreviewRail>\n      ) : (\n        viewport\n      )}\n    </div>\n  );\n}\n"},{"path":"components/agents/prompt-input.tsx","type":"registry:component","target":"@components/agents/prompt-input.tsx","content":"\"use client\";\n// beui.dev/components/agents/chat-app\n\nimport { ArrowUp, Plus, Square } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport {\n  type FormEvent,\n  type KeyboardEvent,\n  type ReactNode,\n  type TextareaHTMLAttributes,\n  useCallback,\n  useEffect,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { Button } from \"@/components/motion/button\";\nimport {\n  MorphPopover,\n  MorphPopoverContent,\n  MorphPopoverTrigger,\n} from \"@/components/motion/popover-morph\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n} from \"@/components/motion/select\";\nimport { SPRING_SWAP } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface PromptModel {\n  value: string;\n  label: ReactNode;\n  icon?: ReactNode;\n  disabled?: boolean;\n}\n\nexport interface PromptAction {\n  value: string;\n  label: ReactNode;\n  description?: ReactNode;\n  icon?: ReactNode;\n  disabled?: boolean;\n}\n\nexport interface PromptInputProps extends Omit<\n  TextareaHTMLAttributes<HTMLTextAreaElement>,\n  \"value\" | \"defaultValue\" | \"onChange\" | \"onSubmit\" | \"children\"\n> {\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  models?: PromptModel[];\n  model?: string;\n  defaultModel?: string;\n  onModelChange?: (model: string) => void;\n  actions?: PromptAction[];\n  onAction?: (action: string) => void;\n  onSubmit?: (value: string, model?: string) => void | Promise<void>;\n  loading?: boolean;\n  onStop?: () => void;\n  minRows?: number;\n  maxRows?: number;\n  leadingAction?: ReactNode;\n  className?: string;\n}\n\nexport function PromptInput({\n  value,\n  defaultValue = \"\",\n  onValueChange,\n  models = [],\n  model,\n  defaultModel,\n  onModelChange,\n  actions = [],\n  onAction,\n  onSubmit,\n  loading = false,\n  onStop,\n  minRows = 2,\n  maxRows = 8,\n  leadingAction,\n  className,\n  disabled,\n  placeholder = \"Ask the agent to do something…\",\n  \"aria-label\": ariaLabel = \"Prompt\",\n  onKeyDown,\n  ...textareaProps\n}: PromptInputProps) {\n  const reduce = useReducedMotion() ?? false;\n  const textareaRef = useRef<HTMLTextAreaElement>(null);\n  const measurementRef = useRef<HTMLDivElement>(null);\n  const [internalValue, setInternalValue] = useState(defaultValue);\n  const [internalModel, setInternalModel] = useState(\n    defaultModel ?? models[0]?.value,\n  );\n  const [actionsOpen, setActionsOpen] = useState(false);\n  const currentValue = value ?? internalValue;\n  const currentModelValue = model ?? internalModel;\n  const currentModel = models.find(\n    (option) => option.value === currentModelValue,\n  );\n  const canSubmit = Boolean(currentValue.trim()) && !disabled && !loading;\n\n  const resizeTextarea = useCallback(() => {\n    const textarea = textareaRef.current;\n    const measurement = measurementRef.current;\n    if (!textarea || !measurement || textarea.value !== currentValue) return;\n\n    const lineHeight = 24;\n    const nextHeight = Math.min(\n      Math.max(measurement.scrollHeight, minRows * lineHeight),\n      maxRows * lineHeight,\n    );\n    const height = `${nextHeight}px`;\n    if (textarea.style.height !== height) textarea.style.height = height;\n  }, [currentValue, maxRows, minRows]);\n\n  useLayoutEffect(() => {\n    resizeTextarea();\n  }, [resizeTextarea]);\n\n  useEffect(() => {\n    const textarea = textareaRef.current;\n    if (!textarea || typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver(resizeTextarea);\n    observer.observe(textarea);\n    return () => observer.disconnect();\n  }, [resizeTextarea]);\n\n  const setValue = (next: string) => {\n    if (value === undefined) setInternalValue(next);\n    onValueChange?.(next);\n  };\n\n  const setModel = (next: string) => {\n    if (model === undefined) setInternalModel(next);\n    onModelChange?.(next);\n  };\n\n  const submit = (event?: FormEvent) => {\n    event?.preventDefault();\n    const prompt = currentValue.trim();\n    if (!prompt || disabled || loading) return;\n\n    onSubmit?.(prompt, currentModelValue);\n    if (value === undefined) setInternalValue(\"\");\n    textareaRef.current?.focus({ preventScroll: true });\n  };\n\n  const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {\n    onKeyDown?.(event);\n    if (\n      event.defaultPrevented ||\n      event.key !== \"Enter\" ||\n      event.shiftKey ||\n      event.nativeEvent.isComposing\n    ) {\n      return;\n    }\n    event.preventDefault();\n    submit();\n  };\n\n  return (\n    <form\n      onSubmit={submit}\n      className={cn(\n        \"relative w-full rounded-2xl border border-border/80 bg-background p-2 transition-colors focus-within:border-foreground/25\",\n        disabled && \"opacity-60\",\n        className,\n      )}\n    >\n      <div\n        ref={measurementRef}\n        aria-hidden=\"true\"\n        className=\"pointer-events-none invisible absolute inset-x-2 top-0 whitespace-pre-wrap px-2 text-sm leading-6 [overflow-wrap:break-word]\"\n      >\n        {`${currentValue}\\u200b`}\n      </div>\n      <textarea\n        ref={textareaRef}\n        value={currentValue}\n        disabled={disabled}\n        placeholder={placeholder}\n        aria-label={ariaLabel}\n        rows={minRows}\n        {...textareaProps}\n        onChange={(event) => setValue(event.target.value)}\n        onKeyDown={handleKeyDown}\n        className=\"scrollbar-hide block w-full resize-none overflow-y-auto bg-transparent px-2 pt-1.5 text-sm leading-6 text-foreground outline-none placeholder:text-muted-foreground/55\"\n      />\n\n      <div className=\"mt-1 flex min-h-8 items-center gap-1\">\n        {actions.length ? (\n          <MorphPopover open={actionsOpen} onOpenChange={setActionsOpen}>\n            <MorphPopoverTrigger>\n              <Button\n                type=\"button\"\n                variant=\"ghost\"\n                size=\"icon\"\n                disabled={disabled || loading}\n                aria-label=\"Add to prompt\"\n                className=\"size-8 rounded-full\"\n              >\n                <motion.span\n                  aria-hidden=\"true\"\n                  animate={{ rotate: actionsOpen ? 45 : 0 }}\n                  transition={reduce ? { duration: 0 } : SPRING_SWAP}\n                >\n                  <Plus className=\"size-4\" />\n                </motion.span>\n              </Button>\n            </MorphPopoverTrigger>\n\n            <MorphPopoverContent\n              side=\"top\"\n              align=\"start\"\n              sideOffset={8}\n              radius={12}\n              className=\"w-56 p-1.5\"\n            >\n              {actions.map((action) => (\n                <button\n                  key={action.value}\n                  type=\"button\"\n                  disabled={action.disabled}\n                  onClick={() => {\n                    onAction?.(action.value);\n                    setActionsOpen(false);\n                  }}\n                  className=\"flex w-full items-start gap-2.5 rounded-lg px-2.5 py-2 text-left outline-none transition-colors hover:bg-muted focus-visible:bg-muted disabled:pointer-events-none disabled:opacity-50\"\n                >\n                  {action.icon ? (\n                    <span className=\"mt-0.5 grid size-5 shrink-0 place-items-center text-muted-foreground [&_svg]:size-4\">\n                      {action.icon}\n                    </span>\n                  ) : null}\n                  <span className=\"min-w-0\">\n                    <span className=\"block text-sm text-foreground\">\n                      {action.label}\n                    </span>\n                    {action.description ? (\n                      <span className=\"mt-0.5 block text-xs leading-4 text-muted-foreground\">\n                        {action.description}\n                      </span>\n                    ) : null}\n                  </span>\n                </button>\n              ))}\n            </MorphPopoverContent>\n          </MorphPopover>\n        ) : null}\n        {leadingAction}\n        {models.length ? (\n          <Select\n            value={currentModelValue}\n            onValueChange={setModel}\n            disabled={disabled || loading}\n            className=\"min-w-0\"\n          >\n            <SelectTrigger className=\"h-8 w-auto max-w-52 rounded-xl border-0 bg-transparent px-2 py-0 text-xs hover:bg-muted focus-visible:ring-2\">\n              <span className=\"flex min-w-0 items-center gap-1.5\">\n                {currentModel?.icon ? (\n                  <span className=\"grid size-4 shrink-0 place-items-center text-muted-foreground [&_svg]:size-3.5\">\n                    {currentModel.icon}\n                  </span>\n                ) : null}\n                <span className=\"truncate text-muted-foreground\">\n                  {currentModel?.label ?? \"Choose model\"}\n                </span>\n              </span>\n            </SelectTrigger>\n            <SelectContent className=\"right-auto w-52 shadow-none\">\n              {models.map((option) => (\n                <SelectItem\n                  key={option.value}\n                  value={option.value}\n                  disabled={option.disabled}\n                  className=\"py-2\"\n                >\n                  <span className=\"flex min-w-0 items-center gap-2\">\n                    {option.icon ? (\n                      <span className=\"grid size-5 shrink-0 place-items-center text-muted-foreground [&_svg]:size-4\">\n                        {option.icon}\n                      </span>\n                    ) : null}\n                    <span className=\"min-w-0 truncate text-sm text-foreground\">\n                      {option.label}\n                    </span>\n                  </span>\n                </SelectItem>\n              ))}\n            </SelectContent>\n          </Select>\n        ) : null}\n\n        <Button\n          type={loading ? \"button\" : \"submit\"}\n          size=\"icon\"\n          disabled={loading ? !onStop : !canSubmit}\n          aria-label={loading ? \"Stop generating\" : \"Send prompt\"}\n          onClick={loading ? onStop : undefined}\n          className=\"ml-auto size-8 rounded-full\"\n        >\n          <AnimatePresence initial={false} mode=\"popLayout\">\n            <motion.span\n              key={loading ? \"stop\" : \"send\"}\n              initial={reduce ? { opacity: 1 } : { opacity: 0, y: 3, scale: 0.8 }}\n              animate={{ opacity: 1, y: 0, scale: 1 }}\n              exit={reduce ? { opacity: 0 } : { opacity: 0, y: -3, scale: 0.8 }}\n              transition={reduce ? { duration: 0 } : SPRING_SWAP}\n              className=\"grid place-items-center\"\n            >\n              {loading ? (\n                <Square className=\"size-3 fill-current\" />\n              ) : (\n                <ArrowUp className=\"size-4\" />\n              )}\n            </motion.span>\n          </AnimatePresence>\n        </Button>\n      </div>\n    </form>\n  );\n}\n"},{"path":"components/agents/streaming-response.tsx","type":"registry:component","target":"@components/agents/streaming-response.tsx","content":"\"use client\";\n// beui.dev/components/agents/chat-app\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/todo-list.tsx","type":"registry:component","target":"@components/agents/todo-list.tsx","content":"\"use client\";\n// beui.dev/components/agents/chat-app\n\nimport { ChevronDown, ListTodo } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport {\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { ActionSwapRollText } from \"@/components/motion/action-swap-roll\";\nimport { AgentDisclosure } from \"@/components/agents/agent-disclosure\";\nimport {\n  EASE_OUT,\n  SPRING_LAYOUT,\n  SPRING_SWAP,\n} from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport type TodoItemStatus =\n  | \"pending\"\n  | \"in-progress\"\n  | \"completed\"\n  | \"cancelled\";\n\nexport interface TodoItem {\n  id: string;\n  title: ReactNode;\n  status?: TodoItemStatus;\n  progress?: number;\n  detail?: ReactNode;\n}\n\nexport interface TodoListProps {\n  items: TodoItem[];\n  title?: ReactNode;\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  collapseOnComplete?: boolean;\n  maxHeight?: number;\n  className?: string;\n}\n\nfunction statusLabel(status: TodoItemStatus) {\n  if (status === \"in-progress\") return \"In progress\";\n  if (status === \"completed\") return \"Completed\";\n  if (status === \"cancelled\") return \"Cancelled\";\n  return \"Pending\";\n}\n\nfunction TodoHeaderIcon({ complete }: { complete: boolean }) {\n  const reduce = useReducedMotion() ?? false;\n\n  return (\n    <span\n      aria-hidden=\"true\"\n      className=\"relative grid size-6 shrink-0 place-items-center\"\n    >\n      <AnimatePresence initial={false} mode=\"popLayout\">\n        {complete ? (\n          <motion.svg\n            key=\"complete\"\n            viewBox=\"0 0 24 24\"\n            initial={reduce ? { opacity: 1 } : { opacity: 0, scale: 0.72 }}\n            animate={{ opacity: 1, scale: 1 }}\n            exit={{ opacity: 0 }}\n            transition={reduce ? { duration: 0 } : SPRING_SWAP}\n            className=\"absolute size-5.5 overflow-visible text-emerald-500\"\n          >\n            <circle cx=\"12\" cy=\"12\" r=\"9\" fill=\"currentColor\" />\n            <motion.path\n              d=\"M7.5 12.25 10.5 15.25 16.75 8.75\"\n              fill=\"none\"\n              stroke=\"white\"\n              strokeWidth=\"2.25\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              initial={reduce ? { pathLength: 1 } : { pathLength: 0 }}\n              animate={{ pathLength: 1 }}\n              transition={\n                reduce ? { duration: 0 } : { duration: 0.24, ease: EASE_OUT }\n              }\n            />\n          </motion.svg>\n        ) : (\n          <motion.span\n            key=\"todo\"\n            initial={reduce ? { opacity: 1 } : { opacity: 0, scale: 0.8 }}\n            animate={{ opacity: 1, scale: 1 }}\n            exit={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.72 }}\n            transition={reduce ? { duration: 0 } : SPRING_SWAP}\n            className=\"absolute grid place-items-center text-muted-foreground\"\n          >\n            <ListTodo className=\"size-4\" />\n          </motion.span>\n        )}\n      </AnimatePresence>\n    </span>\n  );\n}\n\nfunction TodoStatusIcon({\n  status,\n  progress,\n}: {\n  status: TodoItemStatus;\n  progress?: number;\n}) {\n  const reduce = useReducedMotion() ?? false;\n  const normalizedProgress =\n    progress === undefined ? 0.68 : Math.min(100, Math.max(0, progress)) / 100;\n\n  return (\n    <motion.svg\n      aria-hidden=\"true\"\n      viewBox=\"0 0 24 24\"\n      initial={false}\n      className={cn(\n        \"mx-0.5 size-5 shrink-0 overflow-visible text-muted-foreground\",\n        status === \"in-progress\" && \"text-foreground\",\n        status === \"cancelled\" && \"text-rose-600 dark:text-rose-400\",\n      )}\n    >\n      <motion.circle\n        cx=\"12\"\n        cy=\"12\"\n        r=\"9\"\n        fill=\"currentColor\"\n        stroke=\"currentColor\"\n        strokeWidth=\"1.5\"\n        strokeDasharray={status === \"pending\" ? \"2 3\" : undefined}\n        strokeLinecap=\"round\"\n        initial={false}\n        animate={{ fillOpacity: status === \"completed\" ? 0.06 : 0 }}\n        transition={reduce ? { duration: 0 } : { duration: 0.18, ease: EASE_OUT }}\n        className={cn(status === \"in-progress\" && \"opacity-20\")}\n      />\n      <motion.circle\n        cx=\"12\"\n        cy=\"12\"\n        r=\"9\"\n        pathLength=\"1\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2\"\n        strokeLinecap=\"round\"\n        initial={false}\n        animate={{\n          pathLength: status === \"in-progress\" ? normalizedProgress : 0,\n          opacity: status === \"in-progress\" ? 1 : 0,\n          rotate:\n            status === \"in-progress\" && progress === undefined && !reduce\n              ? 360\n              : -90,\n        }}\n        transition={\n          status === \"in-progress\" && progress === undefined && !reduce\n            ? { rotate: { duration: 1.1, repeat: Infinity, ease: \"linear\" } }\n            : reduce\n              ? { duration: 0 }\n              : SPRING_LAYOUT\n        }\n        style={{ transformOrigin: \"12px 12px\" }}\n      />\n      <motion.path\n        d=\"M7.5 12.25 10.5 15.25 16.75 8.75\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        initial={false}\n        animate={{\n          pathLength: status === \"completed\" ? 1 : 0,\n          opacity: status === \"completed\" ? 1 : 0,\n        }}\n        transition={reduce ? { duration: 0 } : { duration: 0.24, ease: EASE_OUT }}\n      />\n      <motion.path\n        d=\"M8.5 8.5 15.5 15.5M15.5 8.5 8.5 15.5\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2\"\n        strokeLinecap=\"round\"\n        initial={false}\n        animate={{\n          pathLength: status === \"cancelled\" ? 1 : 0,\n          opacity: status === \"cancelled\" ? 1 : 0,\n        }}\n        transition={reduce ? { duration: 0 } : { duration: 0.2, ease: EASE_OUT }}\n      />\n    </motion.svg>\n  );\n}\n\nexport function TodoList({\n  items,\n  title = \"To-dos\",\n  open,\n  defaultOpen = true,\n  onOpenChange,\n  collapseOnComplete = true,\n  maxHeight = 248,\n  className,\n}: TodoListProps) {\n  const reduce = useReducedMotion() ?? false;\n  const baseId = useId();\n  const triggerId = `${baseId}-trigger`;\n  const contentId = `${baseId}-content`;\n  const viewportRef = useRef<HTMLDivElement>(null);\n  const previousComplete = useRef(false);\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const currentOpen = open ?? internalOpen;\n  const completed = items.filter((item) => item.status === \"completed\").length;\n  const allComplete = items.length > 0 && completed === items.length;\n  const itemCount = items.length;\n\n  const setOpen = useCallback(\n    (next: boolean) => {\n      if (open === undefined) setInternalOpen(next);\n      onOpenChange?.(next);\n    },\n    [onOpenChange, open],\n  );\n\n  useEffect(() => {\n    if (previousComplete.current && !allComplete) {\n      setOpen(true);\n    }\n    if (!previousComplete.current && allComplete && collapseOnComplete) {\n      setOpen(false);\n    }\n    previousComplete.current = allComplete;\n  }, [allComplete, collapseOnComplete, setOpen]);\n\n  useLayoutEffect(() => {\n    const viewport = viewportRef.current;\n    if (!viewport || itemCount === 0) return;\n\n    const frame = requestAnimationFrame(() => {\n      if (viewport.scrollHeight <= viewport.clientHeight) return;\n      if (typeof viewport.scrollTo === \"function\") {\n        viewport.scrollTo({\n          top: viewport.scrollHeight,\n          behavior: reduce ? \"auto\" : \"smooth\",\n        });\n      } else {\n        viewport.scrollTop = viewport.scrollHeight;\n      }\n    });\n    return () => cancelAnimationFrame(frame);\n  }, [itemCount, reduce]);\n\n  return (\n    <section\n      aria-label=\"Agent task list\"\n      className={cn(\n        \"w-full overflow-hidden rounded-2xl border border-border/70\",\n        className,\n      )}\n    >\n      <button\n        id={triggerId}\n        type=\"button\"\n        aria-expanded={currentOpen}\n        aria-controls={contentId}\n        onClick={() => setOpen(!currentOpen)}\n        className=\"group flex h-11 w-full items-center gap-2.5 rounded-2xl px-3.5 text-left outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n      >\n        <TodoHeaderIcon complete={allComplete} />\n        <h3 className=\"min-w-0 flex-1 truncate text-sm font-medium text-foreground/90\">\n          {title}\n        </h3>\n        <span\n          className={cn(\n            \"shrink-0 text-xs font-medium tabular-nums text-muted-foreground\",\n            allComplete && \"text-emerald-600 dark:text-emerald-400\",\n          )}\n        >\n          <span className=\"sr-only\">\n            {completed} of {items.length} tasks completed\n          </span>\n          <span aria-hidden=\"true\" className=\"inline-flex\">\n            <ActionSwapRollText value={String(completed)}>\n              {completed}\n            </ActionSwapRollText>\n            <span>/</span>\n            <span>{items.length}</span>\n          </span>\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/50 transition-colors group-hover:text-muted-foreground\"\n        >\n          <ChevronDown className=\"size-3.5\" />\n        </motion.span>\n      </button>\n\n      <AgentDisclosure\n        id={contentId}\n        role=\"region\"\n        aria-labelledby={triggerId}\n        open={currentOpen}\n      >\n        <div\n          ref={viewportRef}\n          className=\"scrollbar-hide overflow-y-auto px-2 pb-2\"\n          style={{ maxHeight }}\n        >\n          {items.length ? (\n            <ol aria-live=\"polite\" className=\"space-y-0\">\n            <AnimatePresence initial={false} mode=\"popLayout\">\n              {items.map((item) => {\n                const status = item.status ?? \"pending\";\n                return (\n                  <motion.li\n                    layout=\"position\"\n                    key={item.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                    className=\"flex min-h-9 items-center gap-2.5 rounded-xl px-1.5 py-1\"\n                  >\n                    <TodoStatusIcon status={status} progress={item.progress} />\n                    <span className=\"sr-only\">{statusLabel(status)}: </span>\n                    <span\n                      className={cn(\n                        \"min-w-0 flex-1 truncate text-sm leading-5\",\n                        status === \"pending\" && \"text-muted-foreground/65\",\n                        status === \"in-progress\" && \"text-foreground\",\n                        status === \"completed\" && \"text-muted-foreground/60\",\n                        status === \"cancelled\" && \"text-muted-foreground/55\",\n                      )}\n                    >\n                      <span className=\"relative inline-block max-w-full\">\n                        {item.title}\n                        <motion.span\n                          aria-hidden=\"true\"\n                          initial={false}\n                          animate={{\n                            scaleX: status === \"completed\" ? 1 : 0,\n                            opacity: status === \"completed\" ? 1 : 0,\n                          }}\n                          transition={\n                            reduce\n                              ? { duration: 0 }\n                              : { duration: 0.28, ease: EASE_OUT, delay: 0.06 }\n                          }\n                          className=\"absolute inset-x-0 top-1/2 h-px origin-left bg-current\"\n                        />\n                      </span>\n                    </span>\n                    {item.detail ? (\n                      <span className=\"shrink-0 text-sm text-muted-foreground/55\">\n                        {item.detail}\n                      </span>\n                    ) : null}\n                  </motion.li>\n                );\n              })}\n            </AnimatePresence>\n            </ol>\n          ) : (\n            <p className=\"px-1.5 py-2 text-sm text-muted-foreground\">\n              No tasks yet\n            </p>\n          )}\n        </div>\n      </AgentDisclosure>\n    </section>\n  );\n}\n"},{"path":"components/agents/tool-approval.tsx","type":"registry:component","target":"@components/agents/tool-approval.tsx","content":"\"use client\";\n// beui.dev/components/agents/chat-app\n\nimport {\n  Check,\n  ChevronDown,\n  CircleAlert,\n  LoaderCircle,\n  ShieldCheck,\n  X,\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  AgentCode,\n  type AgentCodeLanguage,\n} from \"@/components/agents/agent-code\";\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 ToolApprovalStatus =\n  | \"pending\"\n  | \"approving\"\n  | \"approved\"\n  | \"denied\"\n  | \"running\"\n  | \"complete\"\n  | \"error\";\n\nexport interface ToolApprovalParameter {\n  id: string;\n  label: ReactNode;\n  value: ReactNode;\n}\n\nexport interface ToolApprovalCodeProps {\n  code: string;\n  language?: AgentCodeLanguage;\n  className?: string;\n}\n\nexport interface ToolApprovalProps {\n  tool: ReactNode;\n  title?: ReactNode;\n  description?: ReactNode;\n  parameters?: ToolApprovalParameter[];\n  status?: ToolApprovalStatus;\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  onApprove?: () => void;\n  onAlwaysAllow?: () => void;\n  onDeny?: () => void;\n  className?: string;\n}\n\nfunction getStatusCopy(status: ToolApprovalStatus) {\n  if (status === \"approving\") return \"Approving\";\n  if (status === \"approved\") return \"Approved\";\n  if (status === \"denied\") return \"Denied\";\n  if (status === \"running\") return \"Running\";\n  if (status === \"complete\") return \"Completed\";\n  if (status === \"error\") return \"Failed\";\n  return \"Approval required\";\n}\n\nfunction getStatusBadgeClass(status: ToolApprovalStatus) {\n  if (status === \"pending\") {\n    return \"border-amber-500/30 bg-amber-500/10 text-amber-600 dark:text-amber-400\";\n  }\n  if (status === \"approving\" || status === \"running\") {\n    return \"border-blue-500/30 bg-blue-500/10 text-blue-600 dark:text-blue-400\";\n  }\n  if (status === \"approved\" || status === \"complete\") {\n    return \"border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400\";\n  }\n  return \"border-rose-500/30 bg-rose-500/10 text-rose-600 dark:text-rose-400\";\n}\n\nexport function ToolApprovalCode({\n  code,\n  language = \"bash\",\n  className,\n}: ToolApprovalCodeProps) {\n  return (\n    <AgentCode\n      code={code}\n      language={language}\n      className={cn(\n        \"rounded-lg border border-border/50 bg-muted/30 px-2.5 py-2\",\n        className,\n      )}\n    />\n  );\n}\n\nexport function ToolApproval({\n  tool,\n  title = \"Allow this tool to run?\",\n  description,\n  parameters = [],\n  status = \"pending\",\n  open,\n  defaultOpen = false,\n  onOpenChange,\n  onApprove,\n  onAlwaysAllow,\n  onDeny,\n  className,\n}: ToolApprovalProps) {\n  const reduce = useReducedMotion() ?? false;\n  const baseId = useId();\n  const detailsId = `${baseId}-details`;\n  const previousStatus = useRef(status);\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  const busy = status === \"approving\" || status === \"running\";\n  const pending = status === \"pending\";\n  const error = status === \"error\";\n\n  useEffect(() => {\n    if (previousStatus.current === \"pending\" && status !== \"pending\") {\n      setOpen(false);\n    }\n    previousStatus.current = status;\n  }, [setOpen, status]);\n\n  return (\n    <div\n      data-state={status}\n      aria-busy={busy}\n      className={cn(\n        \"w-full overflow-hidden rounded-2xl border border-border/60 bg-muted/20 text-sm\",\n        className,\n      )}\n    >\n      <div className=\"flex items-start gap-3 p-4\">\n        <span\n          aria-hidden=\"true\"\n          className={cn(\n            \"mt-0.5 grid size-8 shrink-0 place-items-center rounded-xl border border-border/60 bg-background text-muted-foreground\",\n            error && \"text-destructive\",\n          )}\n        >\n          {busy ? (\n            <LoaderCircle className={cn(\"size-4\", !reduce && \"animate-spin\")} />\n          ) : error ? (\n            <CircleAlert className=\"size-4\" />\n          ) : status === \"denied\" ? (\n            <X className=\"size-4\" />\n          ) : status === \"approved\" || status === \"complete\" ? (\n            <Check className=\"size-4\" />\n          ) : (\n            <ShieldCheck className=\"size-4\" />\n          )}\n        </span>\n\n        <div className=\"min-w-0 flex-1\">\n          <div className=\"flex min-w-0 items-start justify-between gap-3\">\n            <div className=\"min-w-0\">\n              <div className=\"font-medium text-foreground\">{title}</div>\n              <div className=\"mt-0.5 truncate font-mono text-xs text-muted-foreground\">\n                {tool}\n              </div>\n            </div>\n            <span\n              className={cn(\n                \"shrink-0 rounded-full border px-2 py-0.5 text-[11px] font-medium transition-colors\",\n                getStatusBadgeClass(status),\n              )}\n            >\n              {getStatusCopy(status)}\n            </span>\n          </div>\n          {description ? (\n            <p className=\"mt-2 leading-5 text-muted-foreground\">{description}</p>\n          ) : null}\n\n          {parameters.length ? (\n            <button\n              type=\"button\"\n              aria-expanded={currentOpen}\n              aria-controls={detailsId}\n              onClick={() => setOpen(!currentOpen)}\n              className=\"mt-2 inline-flex items-center gap-1 rounded-md text-xs font-medium text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n            >\n              View details\n              <motion.span\n                aria-hidden=\"true\"\n                animate={{ rotate: currentOpen ? 180 : 0 }}\n                transition={reduce ? { duration: 0 } : SPRING_SWAP}\n              >\n                <ChevronDown className=\"size-3.5\" />\n              </motion.span>\n            </button>\n          ) : null}\n        </div>\n      </div>\n\n      <AgentDisclosure\n        id={detailsId}\n        open={currentOpen}\n      >\n        <dl className=\"mx-4 mb-4 grid gap-2 rounded-xl border border-border/50 bg-background/70 p-3\">\n          {parameters.map((parameter) => (\n            <div\n              key={parameter.id}\n              className=\"grid grid-cols-[minmax(0,7rem)_minmax(0,1fr)] items-center gap-3 text-xs\"\n            >\n              <dt className=\"text-muted-foreground\">{parameter.label}</dt>\n              <dd className=\"min-w-0 break-words font-mono text-foreground/85\">\n                {parameter.value}\n              </dd>\n            </div>\n          ))}\n        </dl>\n      </AgentDisclosure>\n\n      <AnimatePresence initial={false}>\n        {pending ? (\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=\"flex flex-wrap items-center gap-2 border-t border-border/60 px-4 py-3\"\n          >\n            <motion.button\n              type=\"button\"\n              onClick={onApprove}\n              whileTap={reduce ? undefined : { scale: 0.97 }}\n              transition={SPRING_PRESS}\n              className=\"rounded-xl bg-foreground px-3 py-1.5 text-xs font-medium text-background outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\"\n            >\n              Allow once\n            </motion.button>\n            {onAlwaysAllow ? (\n              <motion.button\n                type=\"button\"\n                onClick={onAlwaysAllow}\n                whileTap={reduce ? undefined : { scale: 0.97 }}\n                transition={SPRING_PRESS}\n                className=\"rounded-xl border border-border/60 bg-background px-3 py-1.5 text-xs font-medium text-foreground outline-none transition-colors hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring\"\n              >\n                Always allow\n              </motion.button>\n            ) : null}\n            <button\n              type=\"button\"\n              onClick={onDeny}\n              className=\"rounded-xl px-3 py-1.5 text-xs font-medium text-muted-foreground outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n            >\n              Deny\n            </button>\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  );\n}\n"},{"path":"components/agents/tool-result.tsx","type":"registry:component","target":"@components/agents/tool-result.tsx","content":"\"use client\";\n// beui.dev/components/agents/chat-app\n\nimport {\n  Ban,\n  Braces,\n  Check,\n  ChevronDown,\n  CircleCheck,\n  CircleX,\n  Copy,\n  LoaderCircle,\n  RotateCcw,\n  SquareTerminal,\n  Wrench,\n} from \"lucide-react\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport {\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport {\n  AgentCode,\n  type AgentCodeLanguage,\n} from \"@/components/agents/agent-code\";\nimport { ActionSwapRollText } from \"@/components/motion/action-swap-roll\";\nimport { AgentDisclosure } from \"@/components/agents/agent-disclosure\";\nimport { SPRING_PRESS, SPRING_SWAP } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport type ToolResultStatus = \"running\" | \"success\" | \"error\" | \"cancelled\";\nexport type ToolResultKind = \"terminal\" | \"request\" | \"custom\";\n\nexport interface ToolResultProps {\n  tool: ReactNode;\n  title: ReactNode;\n  children: ReactNode;\n  status?: ToolResultStatus;\n  kind?: ToolResultKind;\n  meta?: ReactNode;\n  icon?: ReactNode;\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  collapseOnComplete?: boolean;\n  maxHeight?: number;\n  copyText?: string;\n  onCopy?: () => void | Promise<void>;\n  onRetry?: () => void;\n  className?: string;\n  contentClassName?: string;\n}\n\nexport interface ToolResultOutputProps {\n  children: string;\n  language?: AgentCodeLanguage;\n  className?: string;\n}\n\nfunction getStatusLabel(status: ToolResultStatus) {\n  if (status === \"running\") return \"Running\";\n  if (status === \"success\") return \"Completed\";\n  if (status === \"error\") return \"Failed\";\n  return \"Cancelled\";\n}\n\nfunction getSwapKey(value: ReactNode, fallback: string) {\n  return typeof value === \"string\" || typeof value === \"number\"\n    ? String(value)\n    : fallback;\n}\n\nfunction getStatusClass(status: ToolResultStatus) {\n  if (status === \"running\") {\n    return \"text-blue-600 dark:text-blue-400\";\n  }\n  if (status === \"success\") {\n    return \"text-emerald-600 dark:text-emerald-400\";\n  }\n  if (status === \"error\") {\n    return \"text-rose-600 dark:text-rose-400\";\n  }\n  return \"text-muted-foreground\";\n}\n\nfunction KindIcon({ kind }: { kind: ToolResultKind }) {\n  if (kind === \"terminal\") return <SquareTerminal className=\"size-4\" />;\n  if (kind === \"request\") return <Braces className=\"size-4\" />;\n  return <Wrench className=\"size-4\" />;\n}\n\nfunction StatusIcon({\n  status,\n  reduce,\n}: {\n  status: ToolResultStatus;\n  reduce: boolean;\n}) {\n  if (status === \"running\") {\n    return <LoaderCircle className={cn(\"size-3\", !reduce && \"animate-spin\")} />;\n  }\n  if (status === \"success\") return <CircleCheck className=\"size-3\" />;\n  if (status === \"error\") return <CircleX className=\"size-3\" />;\n  return <Ban className=\"size-3\" />;\n}\n\nfunction ToolResultAction({\n  label,\n  onClick,\n  children,\n}: {\n  label: string;\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      onClick={onClick}\n      whileTap={reduce ? undefined : { scale: 0.9 }}\n      transition={SPRING_PRESS}\n      className=\"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    >\n      {children}\n    </motion.button>\n  );\n}\n\nexport function ToolResultOutput({\n  children,\n  language = \"bash\",\n  className,\n}: ToolResultOutputProps) {\n  return (\n    <AgentCode\n      code={children}\n      language={language}\n      className={cn(\n        \"whitespace-pre-wrap break-words text-foreground/80\",\n        className,\n      )}\n    />\n  );\n}\n\nexport function ToolResult({\n  tool,\n  title,\n  children,\n  status = \"running\",\n  kind = \"custom\",\n  meta,\n  icon,\n  open,\n  defaultOpen = true,\n  onOpenChange,\n  collapseOnComplete = true,\n  maxHeight = 220,\n  copyText,\n  onCopy,\n  onRetry,\n  className,\n  contentClassName,\n}: ToolResultProps) {\n  const reduce = useReducedMotion() ?? false;\n  const baseId = useId();\n  const triggerId = `${baseId}-trigger`;\n  const contentId = `${baseId}-content`;\n  const viewportRef = useRef<HTMLDivElement>(null);\n  const previousStatus = useRef(status);\n  const copyTimer = useRef<number | undefined>(undefined);\n  const [copied, setCopied] = useState(false);\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const currentOpen = open ?? internalOpen;\n  const running = status === \"running\";\n  const canCopy = Boolean(copyText || onCopy);\n  const titleKey = getSwapKey(title, status);\n  const metaKey = getSwapKey(meta, `${status}-meta`);\n  const toolKey = getSwapKey(tool, `${status}-tool`);\n  const statusLabel = getStatusLabel(status);\n\n  const setOpen = useCallback(\n    (next: boolean) => {\n      if (open === undefined) setInternalOpen(next);\n      onOpenChange?.(next);\n    },\n    [onOpenChange, open],\n  );\n\n  useEffect(() => {\n    if (previousStatus.current !== \"running\" && status === \"running\") {\n      setOpen(true);\n    }\n    if (\n      previousStatus.current === \"running\" &&\n      status !== \"running\" &&\n      collapseOnComplete\n    ) {\n      setOpen(false);\n    }\n    previousStatus.current = status;\n  }, [collapseOnComplete, setOpen, status]);\n\n  useEffect(\n    () => () => {\n      if (copyTimer.current) window.clearTimeout(copyTimer.current);\n    },\n    [],\n  );\n\n  useLayoutEffect(() => {\n    const viewport = viewportRef.current;\n    if (!viewport || !currentOpen || !running) return;\n\n    const frame = requestAnimationFrame(() => {\n      if (typeof viewport.scrollTo === \"function\") {\n        viewport.scrollTo({\n          top: viewport.scrollHeight,\n          behavior: reduce ? \"auto\" : \"smooth\",\n        });\n      } else {\n        viewport.scrollTop = viewport.scrollHeight;\n      }\n    });\n    return () => cancelAnimationFrame(frame);\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  return (\n    <div\n      data-state={status}\n      aria-busy={running}\n      className={cn(\"w-full text-sm\", className)}\n    >\n      <button\n        id={triggerId}\n        type=\"button\"\n        aria-expanded={currentOpen}\n        aria-controls={contentId}\n        onClick={() => setOpen(!currentOpen)}\n        className=\"group flex min-h-9 w-full items-center gap-2 rounded-md py-1 text-left outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\"\n      >\n        <span\n          aria-hidden=\"true\"\n          className=\"grid size-4 shrink-0 place-items-center text-muted-foreground\"\n        >\n          {icon ?? <KindIcon kind={kind} />}\n        </span>\n        <span className=\"flex min-w-0 flex-1 items-baseline gap-2\">\n          <span className=\"min-w-0 truncate font-medium text-foreground/90\">\n            <ActionSwapRollText value={titleKey}>\n              {title}\n            </ActionSwapRollText>\n          </span>\n          {meta ? (\n            <span className=\"shrink-0 text-xs text-muted-foreground/60\">\n              <ActionSwapRollText value={metaKey}>\n                {meta}\n              </ActionSwapRollText>\n            </span>\n          ) : null}\n          <span className=\"min-w-0 truncate font-mono text-[11px] text-muted-foreground/55\">\n            <ActionSwapRollText value={toolKey}>\n              {tool}\n            </ActionSwapRollText>\n          </span>\n        </span>\n        <span\n          className={cn(\n            \"inline-flex shrink-0 items-center gap-1 text-[11px] font-medium\",\n            getStatusClass(status),\n          )}\n        >\n          <StatusIcon status={status} reduce={reduce} />\n          <ActionSwapRollText value={status}>{statusLabel}</ActionSwapRollText>\n        </span>\n        <motion.span\n          aria-hidden=\"true\"\n          animate={{ rotate: currentOpen ? 180 : 0 }}\n          transition={reduce ? { duration: 0 } : SPRING_SWAP}\n          className=\"shrink-0 text-muted-foreground/50 transition-colors group-hover:text-muted-foreground\"\n        >\n          <ChevronDown className=\"size-3.5\" />\n        </motion.span>\n      </button>\n\n      <AgentDisclosure\n        id={contentId}\n        role=\"region\"\n        aria-labelledby={triggerId}\n        open={currentOpen}\n      >\n        <div className=\"pl-6 pt-1.5\">\n          <div className=\"overflow-hidden rounded-xl bg-muted/80\">\n          <div\n            ref={viewportRef}\n            role=\"log\"\n            aria-live=\"polite\"\n            className=\"scrollbar-hide overflow-y-auto\"\n            style={{ maxHeight }}\n          >\n            <div className={cn(\"p-3\", contentClassName)}>{children}</div>\n          </div>\n\n            {canCopy || onRetry ? (\n              <div className=\"flex items-center gap-0.5 px-2 pb-1.5\">\n              {canCopy ? (\n                <ToolResultAction\n                  label={copied ? \"Copied\" : \"Copy result\"}\n                  onClick={handleCopy}\n                >\n                  {copied ? (\n                    <Check className=\"size-3.5\" />\n                  ) : (\n                    <Copy className=\"size-3.5\" />\n                  )}\n                </ToolResultAction>\n              ) : null}\n              {onRetry ? (\n                <ToolResultAction label=\"Run again\" onClick={onRetry}>\n                  <RotateCcw className=\"size-3.5\" />\n                </ToolResultAction>\n              ) : null}\n              <span className=\"ml-auto text-[11px] text-muted-foreground/55\">\n                <ActionSwapRollText value={status}>\n                  {statusLabel}\n                </ActionSwapRollText>\n              </span>\n              </div>\n            ) : null}\n          </div>\n        </div>\n      </AgentDisclosure>\n    </div>\n  );\n}\n"},{"path":"components/motion/animated-sidebar.tsx","type":"registry:component","target":"@components/motion/animated-sidebar.tsx","content":"\"use client\";\n\nimport { ChevronRight } from \"lucide-react\";\nimport {\n  AnimatePresence,\n  type HTMLMotionProps,\n  motion,\n  useReducedMotion,\n  type Variants,\n} from \"motion/react\";\nimport {\n  type ButtonHTMLAttributes,\n  type CSSProperties,\n  createContext,\n  forwardRef,\n  type HTMLAttributes,\n  type ReactNode,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n  useSyncExternalStore,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { SharedLayoutBg } from \"@/components/motion/shared-layout-bg\";\nimport {\n  EASE_DRAWER,\n  EASE_OUT,\n  SPRING_LAYOUT,\n  SPRING_PRESS,\n} from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\ntype SidebarState = \"expanded\" | \"collapsed\";\ntype SidebarSide = \"left\" | \"right\";\ntype SidebarVariant = \"sidebar\" | \"floating\" | \"inset\";\ntype SidebarCollapsible = \"offcanvas\" | \"icon\" | \"none\";\n\nconst MOBILE_QUERY = \"(max-width: 767px)\";\nconst SIDEBAR_KEYBOARD_SHORTCUT = \"b\";\n\nconst PANEL_TRANSITION = {\n  duration: 0.36,\n  ease: EASE_DRAWER,\n} as const;\n\n// The desktop rail settles at a hard zero-width boundary. Keep the spring\n// critically damped so it cannot overshoot, pause against that boundary, and\n// then snap back during the final frame.\nconst SIDEBAR_MORPH_TRANSITION = {\n  type: \"spring\",\n  stiffness: 380,\n  damping: 35,\n  mass: 0.75,\n} as const;\n\nconst LABEL_ENTER_TRANSITION = {\n  duration: 0.2,\n  delay: 0.08,\n  ease: EASE_OUT,\n} as const;\n\nconst LABEL_EXIT_TRANSITION = {\n  duration: 0.12,\n  ease: EASE_OUT,\n} as const;\n\nconst SUBMENU_TRANSITION = {\n  duration: 0.18,\n  ease: EASE_OUT,\n} as const;\n\nconst SUBMENU_VARIANTS: Variants = {\n  closed: {\n    opacity: 0,\n    clipPath: \"inset(0 0 100% 0 round 8px)\",\n    transition: {\n      duration: 0.14,\n      ease: EASE_OUT,\n      staggerChildren: 0.025,\n      staggerDirection: -1,\n    },\n  },\n  open: {\n    opacity: 1,\n    clipPath: \"inset(0 0 0% 0 round 8px)\",\n    transition: {\n      duration: 0.2,\n      delayChildren: 0.035,\n      ease: EASE_OUT,\n      staggerChildren: 0.045,\n    },\n  },\n};\n\nconst SUBMENU_ITEM_VARIANTS: Variants = {\n  closed: {\n    opacity: 0,\n    y: -6,\n    filter: \"blur(3px)\",\n  },\n  open: {\n    opacity: 1,\n    y: 0,\n    filter: \"blur(0px)\",\n    transition: SUBMENU_TRANSITION,\n  },\n};\n\nconst REDUCED_TRANSITION = {\n  duration: 0.16,\n  ease: EASE_OUT,\n} as const;\n\nconst FOCUSABLE_SELECTOR = [\n  \"a[href]\",\n  \"button:not([disabled])\",\n  \"input:not([disabled])\",\n  \"select:not([disabled])\",\n  \"textarea:not([disabled])\",\n  \"[tabindex]:not([tabindex='-1'])\",\n].join(\",\");\n\nfunction subscribeToMobileQuery(callback: () => void) {\n  const query = window.matchMedia(MOBILE_QUERY);\n  query.addEventListener(\"change\", callback);\n  return () => query.removeEventListener(\"change\", callback);\n}\n\nfunction getMobileSnapshot() {\n  return window.matchMedia(MOBILE_QUERY).matches;\n}\n\nfunction getServerMobileSnapshot() {\n  return false;\n}\n\nfunction useIsMobile() {\n  return useSyncExternalStore(\n    subscribeToMobileQuery,\n    getMobileSnapshot,\n    getServerMobileSnapshot,\n  );\n}\n\ninterface AnimatedSidebarContextValue {\n  isMobile: boolean;\n  layoutId: string;\n  open: boolean;\n  openMobile: boolean;\n  reduce: boolean;\n  setOpen: (open: boolean) => void;\n  setOpenMobile: (open: boolean) => void;\n  state: SidebarState;\n  toggleSidebar: () => void;\n  triggerRef: React.RefObject<HTMLButtonElement | null>;\n}\n\nconst AnimatedSidebarContext =\n  createContext<AnimatedSidebarContextValue | null>(null);\n\ninterface AnimatedSidebarPanelContextValue {\n  collapsed: boolean;\n  collapsible: SidebarCollapsible;\n  side: SidebarSide;\n}\n\nconst AnimatedSidebarPanelContext =\n  createContext<AnimatedSidebarPanelContextValue | null>(null);\n\nexport function useAnimatedSidebar() {\n  const context = useContext(AnimatedSidebarContext);\n  if (!context) {\n    throw new Error(\n      \"useAnimatedSidebar must be used inside AnimatedSidebarProvider.\",\n    );\n  }\n  return context;\n}\n\nfunction useAnimatedSidebarPanel() {\n  const context = useContext(AnimatedSidebarPanelContext);\n  if (!context) {\n    throw new Error(\n      \"Animated Sidebar parts must be used inside AnimatedSidebar.\",\n    );\n  }\n  return context;\n}\n\ntype SidebarProviderStyle = CSSProperties & {\n  \"--sidebar-width\"?: string;\n  \"--sidebar-width-icon\"?: string;\n  \"--sidebar-width-mobile\"?: string;\n};\n\nexport interface AnimatedSidebarProviderProps\n  extends HTMLAttributes<HTMLDivElement> {\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  openMobile?: boolean;\n  defaultOpenMobile?: boolean;\n  onOpenMobileChange?: (open: boolean) => void;\n  style?: SidebarProviderStyle;\n}\n\nexport function AnimatedSidebarProvider({\n  children,\n  open,\n  defaultOpen = true,\n  onOpenChange,\n  openMobile,\n  defaultOpenMobile = false,\n  onOpenMobileChange,\n  className,\n  style,\n  ...props\n}: AnimatedSidebarProviderProps) {\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const [internalOpenMobile, setInternalOpenMobile] =\n    useState(defaultOpenMobile);\n  const isMobile = useIsMobile();\n  const reduce = useReducedMotion() ?? false;\n  const generatedId = useId();\n  const triggerRef = useRef<HTMLButtonElement>(null);\n  const desktopOpen = open ?? internalOpen;\n  const mobileOpen = openMobile ?? internalOpenMobile;\n\n  const setOpen = useCallback(\n    (nextOpen: boolean) => {\n      if (open === undefined) setInternalOpen(nextOpen);\n      onOpenChange?.(nextOpen);\n    },\n    [onOpenChange, open],\n  );\n\n  const setOpenMobile = useCallback(\n    (nextOpen: boolean) => {\n      if (openMobile === undefined) setInternalOpenMobile(nextOpen);\n      onOpenMobileChange?.(nextOpen);\n    },\n    [onOpenMobileChange, openMobile],\n  );\n\n  const toggleSidebar = useCallback(() => {\n    if (isMobile) setOpenMobile(!mobileOpen);\n    else setOpen(!desktopOpen);\n  }, [desktopOpen, isMobile, mobileOpen, setOpen, setOpenMobile]);\n\n  useEffect(() => {\n    const handleShortcut = (event: KeyboardEvent) => {\n      if (\n        event.key.toLowerCase() === SIDEBAR_KEYBOARD_SHORTCUT &&\n        (event.metaKey || event.ctrlKey)\n      ) {\n        event.preventDefault();\n        toggleSidebar();\n      }\n    };\n\n    window.addEventListener(\"keydown\", handleShortcut);\n    return () => window.removeEventListener(\"keydown\", handleShortcut);\n  }, [toggleSidebar]);\n\n  return (\n    <AnimatedSidebarContext.Provider\n      value={{\n        isMobile,\n        layoutId: `${generatedId}-active`,\n        open: desktopOpen,\n        openMobile: mobileOpen,\n        reduce,\n        setOpen,\n        setOpenMobile,\n        state: desktopOpen ? \"expanded\" : \"collapsed\",\n        toggleSidebar,\n        triggerRef,\n      }}\n    >\n      <div\n        {...props}\n        data-slot=\"sidebar-wrapper\"\n        data-state={desktopOpen ? \"expanded\" : \"collapsed\"}\n        style={{\n          \"--sidebar-width\": \"16rem\",\n          \"--sidebar-width-icon\": \"4.25rem\",\n          \"--sidebar-width-mobile\": \"18rem\",\n          ...style,\n        }}\n        className={cn(\n          \"group/sidebar-wrapper flex min-h-svh w-full min-w-0\",\n          className,\n        )}\n      >\n        {children}\n      </div>\n    </AnimatedSidebarContext.Provider>\n  );\n}\n\nfunction MobileSidebar({\n  ariaLabel,\n  children,\n  className,\n  side,\n}: {\n  ariaLabel: string;\n  children: ReactNode;\n  className?: string;\n  side: SidebarSide;\n}) {\n  const context = useAnimatedSidebar();\n  const panelRef = useRef<HTMLDivElement>(null);\n  const [mounted, setMounted] = useState(false);\n\n  useEffect(() => setMounted(true), []);\n\n  useEffect(() => {\n    if (!context.openMobile) return;\n\n    const body = document.body;\n    const scrollY = window.scrollY;\n    const previousBodyStyles = {\n      left: body.style.left,\n      overflow: body.style.overflow,\n      position: body.style.position,\n      right: body.style.right,\n      top: body.style.top,\n    };\n\n    body.style.position = \"fixed\";\n    body.style.top = `-${scrollY}px`;\n    body.style.left = \"0\";\n    body.style.right = \"0\";\n    body.style.overflow = \"hidden\";\n\n    const focusFrame = requestAnimationFrame(() => {\n      const firstFocusable =\n        panelRef.current?.querySelector<HTMLElement>(FOCUSABLE_SELECTOR);\n      (firstFocusable ?? panelRef.current)?.focus({ preventScroll: true });\n    });\n\n    return () => {\n      cancelAnimationFrame(focusFrame);\n      body.style.position = previousBodyStyles.position;\n      body.style.top = previousBodyStyles.top;\n      body.style.left = previousBodyStyles.left;\n      body.style.right = previousBodyStyles.right;\n      body.style.overflow = previousBodyStyles.overflow;\n      window.scrollTo(0, scrollY);\n      context.triggerRef.current?.focus({ preventScroll: true });\n    };\n  }, [context.openMobile, context.triggerRef]);\n\n  if (!mounted) return null;\n\n  return createPortal(\n    <div\n      className={cn(\n        \"pointer-events-none fixed inset-0 z-50 md:hidden\",\n        context.openMobile ? \"visible\" : \"invisible\",\n      )}\n    >\n      <motion.button\n        type=\"button\"\n        aria-label=\"Close sidebar\"\n        tabIndex={context.openMobile ? 0 : -1}\n        initial={false}\n        animate={{ opacity: context.openMobile ? 1 : 0 }}\n        transition={\n          context.reduce ? REDUCED_TRANSITION : PANEL_TRANSITION\n        }\n        onClick={() => context.setOpenMobile(false)}\n        className={cn(\n          \"absolute inset-0 bg-black/40\",\n          context.openMobile\n            ? \"pointer-events-auto\"\n            : \"pointer-events-none\",\n        )}\n      />\n\n      <motion.div\n        ref={panelRef}\n        role=\"dialog\"\n        aria-modal=\"true\"\n        aria-label={ariaLabel}\n        aria-hidden={!context.openMobile}\n        inert={!context.openMobile}\n        tabIndex={-1}\n        data-mobile=\"true\"\n        data-state={context.openMobile ? \"expanded\" : \"collapsed\"}\n        data-side={side}\n        initial={false}\n        animate={{\n          opacity: context.reduce\n            ? context.openMobile\n              ? 1\n              : 0\n            : 1,\n          x: context.reduce\n            ? 0\n            : context.openMobile\n              ? \"0%\"\n              : side === \"left\"\n                ? \"-100%\"\n                : \"100%\",\n        }}\n        transition={\n          context.reduce ? REDUCED_TRANSITION : PANEL_TRANSITION\n        }\n        onKeyDown={(event) => {\n          if (event.key === \"Escape\") {\n            event.preventDefault();\n            context.setOpenMobile(false);\n            return;\n          }\n\n          if (event.key !== \"Tab\") return;\n          const focusable = panelRef.current\n            ? Array.from(\n                panelRef.current.querySelectorAll<HTMLElement>(\n                  FOCUSABLE_SELECTOR,\n                ),\n              )\n            : [];\n\n          if (focusable.length === 0) {\n            event.preventDefault();\n            panelRef.current?.focus();\n            return;\n          }\n\n          const first = focusable[0];\n          const last = focusable[focusable.length - 1];\n          if (event.shiftKey && document.activeElement === first) {\n            event.preventDefault();\n            last.focus();\n          } else if (!event.shiftKey && document.activeElement === last) {\n            event.preventDefault();\n            first.focus();\n          }\n        }}\n        className={cn(\n          \"pointer-events-auto absolute inset-y-0 flex h-dvh w-(--sidebar-width-mobile) max-w-[88vw] flex-col overflow-hidden\",\n          \"border-border bg-background shadow-2xl will-change-transform\",\n          side === \"left\" ? \"left-0 border-r\" : \"right-0 border-l\",\n          !context.openMobile && \"pointer-events-none\",\n          className,\n        )}\n      >\n        <AnimatedSidebarPanelContext.Provider\n          value={{ collapsed: false, collapsible: \"none\", side }}\n        >\n          {children}\n        </AnimatedSidebarPanelContext.Provider>\n      </motion.div>\n    </div>,\n    document.body,\n  );\n}\n\nexport interface AnimatedSidebarProps\n  extends Omit<HTMLMotionProps<\"aside\">, \"children\"> {\n  children?: ReactNode;\n  side?: SidebarSide;\n  variant?: SidebarVariant;\n  collapsible?: SidebarCollapsible;\n  ariaLabel?: string;\n  panelClassName?: string;\n}\n\nexport const AnimatedSidebar = forwardRef<HTMLElement, AnimatedSidebarProps>(\n  function AnimatedSidebar(\n    {\n      side = \"left\",\n      variant = \"sidebar\",\n      collapsible = \"icon\",\n      ariaLabel = \"Sidebar\",\n      children,\n      className,\n      panelClassName,\n      style,\n      ...props\n    },\n    forwardedRef,\n  ) {\n    const context = useAnimatedSidebar();\n    const collapsed = collapsible !== \"none\" && !context.open;\n    const offcanvas = collapsed && collapsible === \"offcanvas\";\n    const width = offcanvas\n      ? \"0px\"\n      : collapsed\n        ? \"var(--sidebar-width-icon)\"\n        : \"var(--sidebar-width)\";\n\n    if (context.isMobile) {\n      return (\n        <MobileSidebar\n          ariaLabel={ariaLabel}\n          className={className}\n          side={side}\n        >\n          {children}\n        </MobileSidebar>\n      );\n    }\n\n    return (\n      <motion.aside\n        {...props}\n        ref={forwardedRef}\n        initial={false}\n        aria-label={ariaLabel}\n        data-slot=\"sidebar\"\n        data-state={collapsed ? \"collapsed\" : \"expanded\"}\n        data-collapsible={collapsible}\n        data-variant={variant}\n        data-side={side}\n        animate={{ width }}\n        transition={\n          context.reduce ? { duration: 0 } : SIDEBAR_MORPH_TRANSITION\n        }\n        style={style}\n        className={cn(\n          \"group/sidebar relative hidden h-auto shrink-0 md:block will-change-[width]\",\n          \"peer\",\n          side === \"right\" && \"order-last\",\n          className,\n        )}\n      >\n        <motion.div\n          initial={false}\n          animate={{\n            opacity: offcanvas ? 0 : 1,\n            x: offcanvas ? (side === \"left\" ? \"-100%\" : \"100%\") : \"0%\",\n          }}\n          transition={\n            context.reduce ? REDUCED_TRANSITION : PANEL_TRANSITION\n          }\n          className={cn(\n            \"sticky top-0 flex h-svh w-full flex-col overflow-hidden bg-background\",\n            collapsible === \"offcanvas\" && \"w-[var(--sidebar-width)]\",\n            variant === \"sidebar\" &&\n              (side === \"left\" ? \"border-border border-r\" : \"border-border border-l\"),\n            variant === \"floating\" &&\n              \"m-2 h-[calc(100svh-1rem)] rounded-2xl border border-border shadow-sm\",\n            variant === \"inset\" && \"m-2 h-[calc(100svh-1rem)] rounded-2xl\",\n            panelClassName,\n          )}\n        >\n          <AnimatedSidebarPanelContext.Provider\n            value={{ collapsed, collapsible, side }}\n          >\n            {children}\n          </AnimatedSidebarPanelContext.Provider>\n        </motion.div>\n      </motion.aside>\n    );\n  },\n);\n\nexport interface AnimatedSidebarTriggerProps\n  extends ButtonHTMLAttributes<HTMLButtonElement> {}\n\nexport const AnimatedSidebarTrigger = forwardRef<\n  HTMLButtonElement,\n  AnimatedSidebarTriggerProps\n>(function AnimatedSidebarTrigger(\n  { className, onClick, type = \"button\", ...props },\n  forwardedRef,\n) {\n  const context = useAnimatedSidebar();\n  const expanded = context.isMobile ? context.openMobile : context.open;\n\n  return (\n    <button\n      {...props}\n      ref={(node) => {\n        context.triggerRef.current = node;\n        if (typeof forwardedRef === \"function\") forwardedRef(node);\n        else if (forwardedRef) forwardedRef.current = node;\n      }}\n      type={type}\n      aria-label={props[\"aria-label\"] ?? \"Toggle sidebar\"}\n      aria-expanded={expanded}\n      data-slot=\"sidebar-trigger\"\n      data-state={expanded ? \"expanded\" : \"collapsed\"}\n      onClick={(event) => {\n        onClick?.(event);\n        if (!event.defaultPrevented) context.toggleSidebar();\n      }}\n      className={cn(\n        \"inline-flex size-10 shrink-0 items-center justify-center rounded-xl outline-none\",\n        \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n        className,\n      )}\n    />\n  );\n});\n\nexport interface AnimatedSidebarCloseProps\n  extends ButtonHTMLAttributes<HTMLButtonElement> {}\n\nexport const AnimatedSidebarClose = forwardRef<\n  HTMLButtonElement,\n  AnimatedSidebarCloseProps\n>(function AnimatedSidebarClose(\n  { className, onClick, type = \"button\", ...props },\n  forwardedRef,\n) {\n  const context = useAnimatedSidebar();\n\n  return (\n    <button\n      {...props}\n      ref={forwardedRef}\n      type={type}\n      aria-label={props[\"aria-label\"] ?? \"Close sidebar\"}\n      onClick={(event) => {\n        onClick?.(event);\n        if (event.defaultPrevented) return;\n        if (context.isMobile) context.setOpenMobile(false);\n        else context.setOpen(false);\n      }}\n      className={cn(\n        \"inline-flex size-10 shrink-0 items-center justify-center rounded-xl outline-none\",\n        \"focus-visible:ring-2 focus-visible:ring-ring\",\n        className,\n      )}\n    />\n  );\n});\n\nexport interface AnimatedSidebarRailProps\n  extends ButtonHTMLAttributes<HTMLButtonElement> {}\n\nexport const AnimatedSidebarRail = forwardRef<\n  HTMLButtonElement,\n  AnimatedSidebarRailProps\n>(function AnimatedSidebarRail(\n  { className, onClick, type = \"button\", ...props },\n  forwardedRef,\n) {\n  const context = useAnimatedSidebar();\n  const panel = useAnimatedSidebarPanel();\n\n  return (\n    <button\n      {...props}\n      ref={forwardedRef}\n      type={type}\n      data-side={panel.side}\n      aria-label={props[\"aria-label\"] ?? \"Toggle sidebar\"}\n      title=\"Toggle sidebar\"\n      tabIndex={-1}\n      onClick={(event) => {\n        onClick?.(event);\n        if (!event.defaultPrevented) context.toggleSidebar();\n      }}\n      className={cn(\n        \"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 outline-none md:block\",\n        \"after:absolute after:inset-y-0 after:left-1/2 after:w-px after:bg-transparent after:transition-colors hover:after:bg-border\",\n        \"data-[side=right]:right-0 data-[side=right]:translate-x-1/2 data-[side=left]:left-full\",\n        className,\n      )}\n    />\n  );\n});\n\nexport interface AnimatedSidebarInsetProps\n  extends HTMLMotionProps<\"main\"> {}\n\nexport const AnimatedSidebarInset = forwardRef<\n  HTMLElement,\n  AnimatedSidebarInsetProps\n>(function AnimatedSidebarInset({ className, ...props }, forwardedRef) {\n  return (\n    <motion.main\n      {...props}\n      ref={forwardedRef}\n      data-slot=\"sidebar-inset\"\n      className={cn(\n        \"relative flex min-h-svh min-w-0 flex-1 flex-col bg-background\",\n        \"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-2xl md:peer-data-[variant=inset]:shadow-sm\",\n        className,\n      )}\n    />\n  );\n});\n\nexport const AnimatedSidebarHeader = forwardRef<\n  HTMLDivElement,\n  HTMLAttributes<HTMLDivElement>\n>(function AnimatedSidebarHeader({ className, ...props }, forwardedRef) {\n  return (\n    <div\n      {...props}\n      ref={forwardedRef}\n      data-slot=\"sidebar-header\"\n      className={cn(\"flex shrink-0 flex-col gap-2 p-3\", className)}\n    />\n  );\n});\n\nexport const AnimatedSidebarContent = forwardRef<\n  HTMLDivElement,\n  HTMLAttributes<HTMLDivElement>\n>(function AnimatedSidebarContent({ className, ...props }, forwardedRef) {\n  return (\n    <div\n      {...props}\n      ref={forwardedRef}\n      data-slot=\"sidebar-content\"\n      className={cn(\n        \"flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto overflow-x-hidden overscroll-contain px-2 py-2\",\n        className,\n      )}\n    />\n  );\n});\n\nexport const AnimatedSidebarFooter = forwardRef<\n  HTMLDivElement,\n  HTMLAttributes<HTMLDivElement>\n>(function AnimatedSidebarFooter({ className, ...props }, forwardedRef) {\n  return (\n    <div\n      {...props}\n      ref={forwardedRef}\n      data-slot=\"sidebar-footer\"\n      className={cn(\n        \"flex shrink-0 flex-col gap-2 border-border border-t p-3 pb-[max(0.75rem,env(safe-area-inset-bottom))]\",\n        className,\n      )}\n    />\n  );\n});\n\nexport const AnimatedSidebarGroup = forwardRef<\n  HTMLDivElement,\n  HTMLAttributes<HTMLDivElement>\n>(function AnimatedSidebarGroup({ className, ...props }, forwardedRef) {\n  return (\n    <div\n      {...props}\n      ref={forwardedRef}\n      data-slot=\"sidebar-group\"\n      className={cn(\"flex w-full min-w-0 flex-col px-1 py-1.5\", className)}\n    />\n  );\n});\n\nexport const AnimatedSidebarGroupLabel = forwardRef<\n  HTMLDivElement,\n  HTMLAttributes<HTMLDivElement>\n>(function AnimatedSidebarGroupLabel(\n  { children, className, ...props },\n  forwardedRef,\n) {\n  const { collapsed } = useAnimatedSidebarPanel();\n\n  return (\n    <div\n      {...props}\n      ref={forwardedRef}\n      aria-hidden={collapsed}\n      data-slot=\"sidebar-group-label\"\n      className={cn(\n        \"mb-1 h-7 overflow-hidden px-2 text-[10px] font-medium uppercase tracking-[0.14em] text-muted-foreground transition-opacity\",\n        collapsed ? \"opacity-0\" : \"opacity-100\",\n        className,\n      )}\n    >\n      {children}\n    </div>\n  );\n});\n\nexport const AnimatedSidebarGroupContent = forwardRef<\n  HTMLDivElement,\n  HTMLAttributes<HTMLDivElement>\n>(function AnimatedSidebarGroupContent(\n  { className, ...props },\n  forwardedRef,\n) {\n  return (\n    <div\n      {...props}\n      ref={forwardedRef}\n      data-slot=\"sidebar-group-content\"\n      className={cn(\"w-full min-w-0\", className)}\n    />\n  );\n});\n\nexport const AnimatedSidebarMenu = forwardRef<\n  HTMLUListElement,\n  HTMLAttributes<HTMLUListElement>\n>(function AnimatedSidebarMenu(\n  { children, className, ...props },\n  forwardedRef,\n) {\n  return (\n    <SharedLayoutBg\n      {...props}\n      ref={forwardedRef as React.Ref<HTMLElement>}\n      as=\"ul\"\n      inset={0}\n      pillClassName=\"rounded-xl bg-muted/70\"\n      pillContainerClassName=\"inset-y-auto top-0 h-9\"\n      data-slot=\"sidebar-menu\"\n      className={cn(\"flex w-full min-w-0 list-none flex-col gap-0.5\", className)}\n    >\n      {children}\n    </SharedLayoutBg>\n  );\n});\n\nexport const AnimatedSidebarMenuItem = forwardRef<\n  HTMLLIElement,\n  HTMLMotionProps<\"li\">\n>(function AnimatedSidebarMenuItem({ className, ...props }, forwardedRef) {\n  return (\n    <motion.li\n      {...props}\n      ref={forwardedRef}\n      layout=\"position\"\n      transition={SPRING_LAYOUT}\n      data-slot=\"sidebar-menu-item\"\n      className={cn(\"relative\", className)}\n    />\n  );\n});\n\nexport interface AnimatedSidebarMenuSubProps\n  extends Omit<HTMLMotionProps<\"ul\">, \"children\"> {\n  open: boolean;\n  children?: ReactNode;\n}\n\nexport const AnimatedSidebarMenuSub = forwardRef<\n  HTMLUListElement,\n  AnimatedSidebarMenuSubProps\n>(function AnimatedSidebarMenuSub(\n  { open, children, className, ...props },\n  forwardedRef,\n) {\n  const context = useAnimatedSidebar();\n  const panel = useAnimatedSidebarPanel();\n\n  return (\n    <AnimatePresence initial={false} mode=\"popLayout\">\n      {open && !panel.collapsed ? (\n        <motion.ul\n          {...props}\n          ref={forwardedRef}\n          key=\"sidebar-submenu\"\n          variants={context.reduce ? undefined : SUBMENU_VARIANTS}\n          initial={context.reduce ? false : \"closed\"}\n          animate={context.reduce ? { opacity: 1 } : \"open\"}\n          exit={context.reduce ? { opacity: 0 } : \"closed\"}\n          transition={context.reduce ? { duration: 0.12 } : undefined}\n          data-slot=\"sidebar-menu-sub\"\n          className={cn(\n            \"relative mt-1 ml-5 flex min-w-0 flex-col gap-0.5 border-border border-l pl-3\",\n            className,\n          )}\n        >\n          {children}\n        </motion.ul>\n      ) : null}\n    </AnimatePresence>\n  );\n});\n\nexport const AnimatedSidebarMenuSubItem = forwardRef<\n  HTMLLIElement,\n  HTMLMotionProps<\"li\">\n>(function AnimatedSidebarMenuSubItem(\n  { className, ...props },\n  forwardedRef,\n) {\n  return (\n    <motion.li\n      {...props}\n      ref={forwardedRef}\n      variants={SUBMENU_ITEM_VARIANTS}\n      data-slot=\"sidebar-menu-sub-item\"\n      className={cn(\"relative min-w-0\", className)}\n    />\n  );\n});\n\nexport interface AnimatedSidebarMenuSubButtonProps {\n  children: ReactNode;\n  icon?: ReactNode;\n  href?: string;\n  isActive?: boolean;\n  disabled?: boolean;\n  closeOnSelect?: boolean;\n  target?: \"_blank\" | \"_self\" | \"_parent\" | \"_top\";\n  rel?: string;\n  onSelect?: () => void;\n  className?: string;\n}\n\nexport function AnimatedSidebarMenuSubButton({\n  children,\n  icon,\n  href,\n  isActive = false,\n  disabled = false,\n  closeOnSelect = true,\n  target,\n  rel,\n  onSelect,\n  className,\n}: AnimatedSidebarMenuSubButtonProps) {\n  const context = useAnimatedSidebar();\n\n  const select = (\n    event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>,\n  ) => {\n    if (disabled) {\n      event.preventDefault();\n      return;\n    }\n    onSelect?.();\n    if (context.isMobile && closeOnSelect) context.setOpenMobile(false);\n  };\n\n  const content = (\n    <>\n      <span\n        aria-hidden=\"true\"\n        className=\"grid size-4 shrink-0 place-items-center\"\n      >\n        {icon ?? <span className=\"size-1 rounded-full bg-current\" />}\n      </span>\n      <span className=\"min-w-0 flex-1 truncate\">{children}</span>\n    </>\n  );\n\n  const interactiveClassName = cn(\n    \"flex min-h-8 w-full min-w-0 items-center gap-2 rounded-lg px-2 text-left text-xs outline-none\",\n    \"text-muted-foreground transition-colors hover:bg-muted/60 hover:text-foreground\",\n    \"focus-visible:bg-muted/70 focus-visible:ring-2 focus-visible:ring-ring\",\n    isActive && \"bg-muted/70 text-foreground\",\n    disabled && \"cursor-not-allowed opacity-40\",\n    className,\n  );\n\n  return href ? (\n    <motion.a\n      href={href}\n      target={target}\n      rel={\n        rel ??\n        (target === \"_blank\" ? \"noreferrer noopener\" : undefined)\n      }\n      aria-current={isActive ? \"page\" : undefined}\n      aria-disabled={disabled || undefined}\n      tabIndex={disabled ? -1 : undefined}\n      onClick={select}\n      whileTap={context.reduce || disabled ? undefined : { scale: 0.98 }}\n      transition={SPRING_PRESS}\n      className={interactiveClassName}\n    >\n      {content}\n    </motion.a>\n  ) : (\n    <motion.button\n      type=\"button\"\n      disabled={disabled}\n      aria-current={isActive ? \"page\" : undefined}\n      onClick={select}\n      whileTap={context.reduce || disabled ? undefined : { scale: 0.98 }}\n      transition={SPRING_PRESS}\n      className={interactiveClassName}\n    >\n      {content}\n    </motion.button>\n  );\n}\n\nexport interface AnimatedSidebarMenuButtonProps {\n  children: ReactNode;\n  icon?: ReactNode;\n  badge?: ReactNode;\n  href?: string;\n  isActive?: boolean;\n  ariaExpanded?: boolean;\n  disabled?: boolean;\n  closeOnSelect?: boolean;\n  target?: \"_blank\" | \"_self\" | \"_parent\" | \"_top\";\n  rel?: string;\n  onSelect?: () => void;\n  className?: string;\n}\n\nexport function AnimatedSidebarMenuButton({\n  children,\n  icon,\n  badge,\n  href,\n  isActive = false,\n  ariaExpanded,\n  disabled = false,\n  closeOnSelect,\n  target,\n  rel,\n  onSelect,\n  className,\n}: AnimatedSidebarMenuButtonProps) {\n  const context = useAnimatedSidebar();\n  const panel = useAnimatedSidebarPanel();\n  const textLabel = typeof children === \"string\" ? children : undefined;\n\n  const select = (\n    event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>,\n  ) => {\n    if (disabled) {\n      event.preventDefault();\n      return;\n    }\n    onSelect?.();\n    const shouldCloseOnSelect =\n      closeOnSelect ?? ariaExpanded === undefined;\n    if (context.isMobile && shouldCloseOnSelect) {\n      context.setOpenMobile(false);\n    }\n  };\n\n  const content = (\n    <>\n      {isActive ? (\n        <motion.span\n          layoutId={context.layoutId}\n          transition={context.reduce ? { duration: 0 } : SPRING_LAYOUT}\n          className=\"absolute inset-0 rounded-xl bg-muted\"\n        />\n      ) : null}\n      {icon ? (\n        <span\n          aria-hidden=\"true\"\n          className=\"relative z-10 grid size-5 shrink-0 place-items-center\"\n        >\n          {icon}\n        </span>\n      ) : null}\n      <motion.span\n        initial={false}\n        animate={{\n          opacity: panel.collapsed ? 0 : 1,\n          x: panel.collapsed ? -4 : 0,\n        }}\n        transition={\n          context.reduce\n            ? REDUCED_TRANSITION\n            : panel.collapsed\n              ? LABEL_EXIT_TRANSITION\n              : LABEL_ENTER_TRANSITION\n        }\n        aria-hidden={panel.collapsed}\n        className={cn(\n          \"relative z-10 min-w-0 flex-1 truncate\",\n          panel.collapsed && \"pointer-events-none\",\n        )}\n      >\n        {children}\n      </motion.span>\n      {badge && !panel.collapsed ? (\n        <span className=\"relative z-10 shrink-0 text-xs text-muted-foreground\">\n          {badge}\n        </span>\n      ) : null}\n      {ariaExpanded !== undefined ? (\n        <motion.span\n          aria-hidden=\"true\"\n          initial={false}\n          animate={{\n            opacity: panel.collapsed ? 0 : 1,\n            rotate: ariaExpanded ? 90 : 0,\n            x: panel.collapsed ? 4 : 0,\n          }}\n          transition={context.reduce ? { duration: 0 } : SPRING_LAYOUT}\n          className=\"relative z-10 grid size-4 shrink-0 place-items-center text-muted-foreground\"\n        >\n          <ChevronRight className=\"size-3.5\" />\n        </motion.span>\n      ) : null}\n    </>\n  );\n\n  const interactiveClassName = cn(\n    \"relative flex min-h-9 w-full min-w-0 items-center gap-2.5 overflow-hidden rounded-xl px-3 text-left text-sm font-medium outline-none\",\n    \"text-muted-foreground transition-colors hover:text-foreground\",\n    \"focus-visible:bg-muted/70 focus-visible:ring-2 focus-visible:ring-ring\",\n    isActive && \"text-foreground\",\n    disabled && \"cursor-not-allowed opacity-40\",\n    className,\n  );\n\n  return href ? (\n    <motion.a\n      href={href}\n      target={target}\n      rel={\n        rel ??\n        (target === \"_blank\" ? \"noreferrer noopener\" : undefined)\n      }\n      aria-current={isActive ? \"page\" : undefined}\n      aria-expanded={ariaExpanded}\n      aria-disabled={disabled || undefined}\n      aria-label={panel.collapsed ? textLabel : undefined}\n      title={panel.collapsed ? textLabel : undefined}\n      tabIndex={disabled ? -1 : undefined}\n      onClick={select}\n      whileTap={context.reduce || disabled ? undefined : { scale: 0.98 }}\n      transition={SPRING_PRESS}\n      className={interactiveClassName}\n    >\n      {content}\n    </motion.a>\n  ) : (\n    <motion.button\n      type=\"button\"\n      disabled={disabled}\n      aria-current={isActive ? \"page\" : undefined}\n      aria-expanded={ariaExpanded}\n      aria-label={panel.collapsed ? textLabel : undefined}\n      title={panel.collapsed ? textLabel : undefined}\n      onClick={select}\n      whileTap={context.reduce || disabled ? undefined : { scale: 0.98 }}\n      transition={SPRING_PRESS}\n      className={interactiveClassName}\n    >\n      {content}\n    </motion.button>\n  );\n}\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":"components/agents/agent-activity/activity-row.tsx","type":"registry:component","target":"@components/agents/agent-activity/activity-row.tsx","content":"import {\n  Check,\n  Circle,\n  FileText,\n  Globe2,\n  ImageIcon,\n  MessageSquare,\n  PencilLine,\n  Search,\n  Sparkles,\n  SquareTerminal,\n  Wrench,\n} from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { EASE_OUT, SPRING_LAYOUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\nimport type {\n  AgentActivityItem,\n  AgentActivitySearch,\n  AgentActivityStep,\n  AgentActivityText,\n  AgentActivityTool,\n  AgentActivityTrace,\n  AgentSearchResult,\n} from \"./types\";\n\nfunction StepRow({ item }: { item: AgentActivityStep }) {\n  const state = item.status ?? \"complete\";\n\n  return (\n    <div className=\"flex min-h-7 items-start gap-2.5 rounded-md px-1.5 py-1\">\n      <span\n        aria-hidden=\"true\"\n        className=\"mt-0.5 grid size-4 shrink-0 place-items-center text-muted-foreground/70\"\n      >\n        {state === \"complete\" ? (\n          <Check className=\"size-4\" strokeWidth={1.8} />\n        ) : state === \"active\" ? (\n          <span className=\"relative grid size-3 place-items-center\">\n            <motion.span\n              className=\"absolute inset-0 rounded-full bg-foreground/10\"\n              animate={{ opacity: [0.35, 0.8, 0.35] }}\n              transition={{ duration: 1.5, repeat: Number.POSITIVE_INFINITY }}\n            />\n            <span className=\"size-1.5 rounded-full bg-foreground/60\" />\n          </span>\n        ) : (\n          <Circle className=\"size-3\" strokeWidth={1.5} />\n        )}\n      </span>\n      <span\n        className={cn(\n          \"min-w-0 flex-1 leading-5\",\n          state === \"pending\" ? \"text-muted-foreground/55\" : \"text-foreground/90\",\n        )}\n      >\n        {item.label}\n      </span>\n      {item.meta ? (\n        <span className=\"shrink-0 leading-5 text-muted-foreground/55\">\n          {item.meta}\n        </span>\n      ) : null}\n    </div>\n  );\n}\n\nfunction TextRow({ item }: { item: AgentActivityText }) {\n  return (\n    <div className=\"rounded-md px-1.5 py-1 leading-5 text-muted-foreground\">\n      {item.content}\n    </div>\n  );\n}\n\nfunction SearchResultRow({\n  result,\n}: {\n  result: AgentSearchResult;\n}) {\n  const content = (\n    <>\n      <span\n        aria-hidden=\"true\"\n        className=\"grid size-5 shrink-0 place-items-center text-muted-foreground\"\n      >\n        {result.icon ?? <Globe2 className=\"size-3\" strokeWidth={2} />}\n      </span>\n      <span className=\"min-w-0 truncate font-medium text-foreground/90\">\n        {result.title}\n      </span>\n      {result.domain ? (\n        <span className=\"min-w-0 truncate text-muted-foreground/55\">\n          {result.domain}\n        </span>\n      ) : null}\n    </>\n  );\n  const className = cn(\n    \"flex min-h-7 items-center gap-2 rounded-md px-1.5 py-1 text-left outline-none transition-colors\",\n    result.url && \"focus-visible:ring-2 focus-visible:ring-ring\",\n  );\n\n  return result.url ? (\n    <a href={result.url} className={className}>\n      {content}\n    </a>\n  ) : (\n    <div className={className}>{content}</div>\n  );\n}\n\nfunction SearchRow({ item }: { item: AgentActivitySearch }) {\n  const reduce = useReducedMotion() ?? false;\n  const enter = reduce ? { opacity: 1 } : { opacity: 0, y: 6 };\n  const visible = { opacity: 1, y: 0 };\n  const exit = reduce ? { opacity: 0 } : { opacity: 0, y: -3 };\n  const transition = reduce\n    ? { duration: 0 }\n    : {\n        opacity: { duration: 0.18, ease: EASE_OUT },\n        y: SPRING_LAYOUT,\n        layout: SPRING_LAYOUT,\n      };\n\n  return (\n    <div className=\"space-y-0.5\">\n      <div className=\"flex min-h-7 items-center gap-2.5 rounded-md px-1.5 py-1 text-muted-foreground\">\n        <Search aria-hidden=\"true\" className=\"size-4 shrink-0\" strokeWidth={1.7} />\n        <span className=\"min-w-0 truncate\">{item.query}</span>\n      </div>\n      {item.results?.length ? (\n        <div className=\"space-y-0.5 pl-4\">\n          <AnimatePresence initial mode=\"popLayout\">\n            {item.results.map((result) => (\n              <motion.div\n                layout=\"position\"\n                key={result.id}\n                initial={enter}\n                animate={visible}\n                exit={exit}\n                transition={transition}\n              >\n                <SearchResultRow result={result} />\n              </motion.div>\n            ))}\n          </AnimatePresence>\n        </div>\n      ) : null}\n      <AnimatePresence initial>\n        {item.moreCount ? (\n          <motion.div\n            key=\"more-results\"\n            initial={enter}\n            animate={visible}\n            exit={exit}\n            transition={transition}\n            className=\"px-1.5 py-1 pl-8 text-muted-foreground/55\"\n          >\n            +{item.moreCount} more\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  );\n}\n\nfunction ActionIcon({ action }: { action: string }) {\n  if (action === \"read\") return <FileText className=\"size-4\" />;\n  if (action === \"edit\" || action === \"write\") {\n    return <PencilLine className=\"size-4\" />;\n  }\n  if (action === \"run\") return <SquareTerminal className=\"size-4\" />;\n  return <Wrench className=\"size-4\" />;\n}\n\nfunction ToolRow({ item }: { item: AgentActivityTool }) {\n  const action = item.action.charAt(0).toUpperCase() + item.action.slice(1);\n\n  return (\n    <div className=\"flex min-h-8 min-w-0 items-center gap-2.5 rounded-md px-1.5 py-0.5 leading-5\">\n      <span\n        aria-hidden=\"true\"\n        className=\"grid size-4 shrink-0 place-items-center text-muted-foreground/70\"\n      >\n        <ActionIcon action={item.action} />\n      </span>\n      <span className=\"shrink-0 font-medium text-foreground/90\">{action}</span>\n      <span className=\"min-w-0 flex-1 truncate rounded-lg bg-muted/80 px-2.5 py-1 font-mono text-xs text-muted-foreground/70\">\n        {item.target}\n      </span>\n      {typeof item.additions === \"number\" || typeof item.deletions === \"number\" ? (\n        <span className=\"flex shrink-0 items-center gap-2 font-mono tabular-nums\">\n          {typeof item.additions === \"number\" ? (\n            <span className=\"text-emerald-500\">+{item.additions}</span>\n          ) : null}\n          {typeof item.deletions === \"number\" ? (\n            <span className=\"text-rose-500\">−{item.deletions}</span>\n          ) : null}\n        </span>\n      ) : null}\n    </div>\n  );\n}\n\nfunction TraceIcon({ kind }: { kind: AgentActivityTrace[\"kind\"] }) {\n  if (kind === \"thinking\") return <Sparkles className=\"size-4\" />;\n  if (kind === \"message\") return <MessageSquare className=\"size-4\" />;\n  if (kind === \"write\") return <PencilLine className=\"size-4\" />;\n  if (kind === \"run\") return <SquareTerminal className=\"size-4\" />;\n  if (kind === \"read\") return <ImageIcon className=\"size-4\" />;\n  return <Wrench className=\"size-4\" />;\n}\n\nfunction TraceRow({ item }: { item: AgentActivityTrace }) {\n  return (\n    <div className=\"grid min-h-8 grid-cols-[1rem_auto_minmax(0,1fr)] items-center gap-2.5 rounded-md px-1.5 py-0.5\">\n      <span\n        aria-hidden=\"true\"\n        className=\"grid size-4 place-items-center text-muted-foreground/70\"\n      >\n        {item.icon ?? <TraceIcon kind={item.kind} />}\n      </span>\n      <span className=\"font-medium text-foreground/90\">{item.label}</span>\n      {item.detail ? (\n        <span className=\"min-w-0 truncate rounded-lg bg-muted/80 px-2.5 py-1 font-mono text-xs text-muted-foreground/70\">\n          {item.detail}\n        </span>\n      ) : (\n        <span />\n      )}\n    </div>\n  );\n}\n\nexport function ActivityRow({ item }: { item: AgentActivityItem }) {\n  if (item.type === \"text\") return <TextRow item={item} />;\n  if (item.type === \"search\") return <SearchRow item={item} />;\n  if (item.type === \"tool\") return <ToolRow item={item} />;\n  if (item.type === \"trace\") return <TraceRow item={item} />;\n  return <StepRow item={item} />;\n}\n"},{"path":"components/agents/agent-activity/types.ts","type":"registry:component","target":"@components/agents/agent-activity/types.ts","content":"import type { ReactNode } from \"react\";\n\nexport type AgentActivityStatus = \"working\" | \"complete\";\nexport type AgentStepStatus = \"pending\" | \"active\" | \"complete\";\n\nexport interface AgentActivityStep {\n  id: string;\n  type: \"step\";\n  label: ReactNode;\n  status?: AgentStepStatus;\n  meta?: ReactNode;\n}\n\nexport interface AgentActivityText {\n  id: string;\n  type: \"text\";\n  content: ReactNode;\n}\n\nexport interface AgentSearchResult {\n  id: string;\n  title: ReactNode;\n  domain?: ReactNode;\n  url?: string;\n  icon?: ReactNode;\n}\n\nexport interface AgentActivitySearch {\n  id: string;\n  type: \"search\";\n  query: ReactNode;\n  results?: AgentSearchResult[];\n  moreCount?: number;\n}\n\nexport interface AgentActivityTool {\n  id: string;\n  type: \"tool\";\n  action: \"read\" | \"edit\" | \"run\" | (string & {});\n  target: ReactNode;\n  additions?: number;\n  deletions?: number;\n}\n\nexport type AgentTraceKind =\n  | \"thinking\"\n  | \"message\"\n  | \"write\"\n  | \"run\"\n  | \"read\"\n  | (string & {});\n\nexport interface AgentActivityTrace {\n  id: string;\n  type: \"trace\";\n  kind: AgentTraceKind;\n  label: ReactNode;\n  detail?: ReactNode;\n  icon?: ReactNode;\n}\n\nexport type AgentActivityItem =\n  | AgentActivityStep\n  | AgentActivityText\n  | AgentActivitySearch\n  | AgentActivityTool\n  | AgentActivityTrace;\n\nexport type AgentActivityContentType = AgentActivityItem[\"type\"] | \"mixed\";\n\nexport interface AgentActivityProps {\n  /** Chronological activity entries. Append or update items as events stream. */\n  items: AgentActivityItem[];\n  /** Expected activity kind before the first streamed item arrives. */\n  contentType?: AgentActivityContentType;\n  /** Current run phase. Active runs always stay expanded. */\n  status?: AgentActivityStatus;\n  /** Elapsed run time, in seconds. Used by the step-only summary. */\n  duration?: number;\n  /** Controlled expanded state used after the run completes. */\n  open?: boolean;\n  /** Initial expanded state used after the run completes. */\n  defaultOpen?: boolean;\n  /** Called when the completed activity disclosure changes state. */\n  onOpenChange?: (open: boolean) => void;\n  /** Collapse the disclosure when status changes from working to complete. */\n  collapseOnComplete?: boolean;\n  /** Optional label shown while the run is active. */\n  activeLabel?: ReactNode;\n  /** Optional completed summary. Derived from the item types by default. */\n  summary?: ReactNode;\n  /** Maximum visible activity height before the stream begins gliding. */\n  maxHeight?: number;\n  className?: string;\n  contentClassName?: string;\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":"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":"components/motion/popover-morph.tsx","type":"registry:component","target":"@components/motion/popover-morph.tsx","content":"\"use client\";\n\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport {\n  cloneElement,\n  createContext,\n  isValidElement,\n  type ReactElement,\n  type ReactNode,\n  type Ref,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { usePopoverPortalPosition } from \"@/components/motion/popover-position\";\nimport { EASE_OUT, SPRING_PANEL } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\ntype Side = \"top\" | \"bottom\";\ntype Align = \"start\" | \"end\";\n\ntype MorphContextValue = {\n  open: boolean;\n  setOpen: (open: boolean) => void;\n  toggle: () => void;\n  triggerId: string;\n  contentId: string;\n  triggerRef: React.MutableRefObject<HTMLElement | null>;\n  contentRef: React.MutableRefObject<HTMLDivElement | null>;\n};\n\nconst MorphContext = createContext<MorphContextValue | null>(null);\n\nfunction useMorphContext(component: string) {\n  const ctx = useContext(MorphContext);\n  if (!ctx) throw new Error(`${component} must be used within <MorphPopover>`);\n  return ctx;\n}\n\nexport interface MorphPopoverProps {\n  children: ReactNode;\n  /** Controlled open state. */\n  open?: boolean;\n  /** Uncontrolled initial open state. */\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  className?: string;\n}\n\n/**\n * A popover whose panel morphs open from the trigger corner: it's laid out at\n * full size but clipped to the corner nearest the trigger, then unclips as one\n * piece. Closes on outside pointer / Escape. Controlled or uncontrolled.\n */\nexport function MorphPopover({\n  children,\n  open: controlledOpen,\n  defaultOpen = false,\n  onOpenChange,\n  className,\n}: MorphPopoverProps) {\n  const baseId = useId();\n  const rootRef = useRef<HTMLDivElement>(null);\n  const triggerRef = useRef<HTMLElement | null>(null);\n  const contentRef = useRef<HTMLDivElement | null>(null);\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const controlled = controlledOpen !== undefined;\n  const open = controlled ? controlledOpen : internalOpen;\n\n  const setOpen = useCallback(\n    (next: boolean) => {\n      if (!controlled) setInternalOpen(next);\n      onOpenChange?.(next);\n    },\n    [controlled, onOpenChange],\n  );\n  const toggle = useCallback(() => setOpen(!open), [setOpen, open]);\n\n  useEffect(() => {\n    if (!open) return;\n    const onKey = (e: KeyboardEvent) => e.key === \"Escape\" && setOpen(false);\n    const onPointer = (e: PointerEvent) => {\n      const target = e.target as Node;\n      if (\n        rootRef.current &&\n        !rootRef.current.contains(target) &&\n        !contentRef.current?.contains(target)\n      )\n        setOpen(false);\n    };\n    window.addEventListener(\"keydown\", onKey);\n    window.addEventListener(\"pointerdown\", onPointer);\n    return () => {\n      window.removeEventListener(\"keydown\", onKey);\n      window.removeEventListener(\"pointerdown\", onPointer);\n    };\n  }, [open, setOpen]);\n\n  const ctx = useMemo<MorphContextValue>(\n    () => ({\n      open,\n      setOpen,\n      toggle,\n      triggerId: `${baseId}-trigger`,\n      contentId: `${baseId}-content`,\n      triggerRef,\n      contentRef,\n    }),\n    [open, setOpen, toggle, baseId],\n  );\n\n  return (\n    <MorphContext.Provider value={ctx}>\n      <div ref={rootRef} className={cn(\"relative inline-flex\", className)}>\n        {children}\n      </div>\n    </MorphContext.Provider>\n  );\n}\n\nexport interface MorphPopoverTriggerProps {\n  children: ReactElement;\n}\n\nfunction mergeRefs<T>(...refs: Array<Ref<T> | undefined>) {\n  return (node: T | null) => {\n    for (const ref of refs) {\n      if (typeof ref === \"function\") ref(node);\n      else if (ref && typeof ref === \"object\")\n        (ref as React.MutableRefObject<T | null>).current = node;\n    }\n  };\n}\n\n/** Wraps a single element, toggling the popover on click. */\nexport function MorphPopoverTrigger({ children }: MorphPopoverTriggerProps) {\n  const ctx = useMorphContext(\"MorphPopoverTrigger\");\n  if (!isValidElement(children)) return children;\n\n  const child = children as ReactElement<Record<string, unknown>>;\n  const childOnClick = child.props.onClick as\n    | ((e: unknown) => void)\n    | undefined;\n  const childRef = (child.props as { ref?: Ref<HTMLElement> }).ref;\n\n  return cloneElement(child, {\n    id: ctx.triggerId,\n    ref: mergeRefs(childRef, (node: HTMLElement | null) => {\n      ctx.triggerRef.current = node;\n    }),\n    onClick: (e: unknown) => {\n      childOnClick?.(e);\n      ctx.toggle();\n    },\n    \"aria-haspopup\": \"dialog\",\n    \"aria-expanded\": ctx.open,\n    \"aria-controls\": ctx.open ? ctx.contentId : undefined,\n  });\n}\n\nconst originFor = (side: Side, align: Align) =>\n  `${side === \"bottom\" ? \"top\" : \"bottom\"} ${align === \"end\" ? \"right\" : \"left\"}`;\n\n// A clip that hides everything but the corner nearest the trigger, so the\n// panel appears to grow out of it. inset(top right bottom left).\nfunction clipHidden(side: Side, align: Align, radius: number) {\n  const top = side === \"bottom\" ? \"0%\" : \"92%\";\n  const bottom = side === \"bottom\" ? \"92%\" : \"0%\";\n  const right = align === \"end\" ? \"0%\" : \"92%\";\n  const left = align === \"end\" ? \"92%\" : \"0%\";\n  return `inset(${top} ${right} ${bottom} ${left} round ${radius}px)`;\n}\nconst clipShown = (radius: number) => `inset(0% 0% 0% 0% round ${radius}px)`;\n\n// Preserve the original spring character on the wrapper, but tween the complex\n// clip-path so it cannot snap when the spring resolves its final distance.\nconst MORPH_CLIP_TRANSITION = { duration: 0.32, ease: EASE_OUT } as const;\n\nexport interface MorphPopoverContentProps {\n  children: ReactNode;\n  side?: Side;\n  align?: Align;\n  /** Gap between trigger and panel, in px. Default 8. */\n  sideOffset?: number;\n  /** Panel corner radius, in px. Default 16. */\n  radius?: number;\n  className?: string;\n}\n\nexport function MorphPopoverContent({\n  children,\n  side = \"bottom\",\n  align = \"end\",\n  sideOffset = 8,\n  radius = 16,\n  className,\n}: MorphPopoverContentProps) {\n  const ctx = useMorphContext(\"MorphPopoverContent\");\n  const reduce = useReducedMotion() ?? false;\n  const [portalReady, setPortalReady] = useState(false);\n  const layout = usePopoverPortalPosition(\n    ctx.triggerRef,\n    ctx.contentRef,\n    portalReady && ctx.open,\n  );\n\n  useEffect(() => setPortalReady(true), []);\n  const left = layout\n    ? align === \"end\"\n      ? layout.trigger.left + layout.trigger.width - layout.content.width\n      : layout.trigger.left\n    : 0;\n  const top = layout\n    ? side === \"bottom\"\n      ? layout.trigger.top + layout.trigger.height + sideOffset\n      : layout.trigger.top - layout.content.height - sideOffset\n    : 0;\n\n  // Both directions travel between the exact same hidden/show states. Exit\n  // targets \"hidden\" directly instead of introducing separate choreography.\n  const wrap = reduce\n    ? undefined\n    : {\n        hidden: { opacity: 0, scale: 0.96, transition: SPRING_PANEL },\n        show: { opacity: 1, scale: 1, transition: SPRING_PANEL },\n      };\n  const clip = reduce\n    ? undefined\n    : {\n        hidden: {\n          clipPath: clipHidden(side, align, radius),\n          transition: MORPH_CLIP_TRANSITION,\n        },\n        show: {\n          clipPath: clipShown(radius),\n          transition: MORPH_CLIP_TRANSITION,\n        },\n      };\n\n  // Keep the server and first client render identical, then mount the portal.\n  if (!portalReady) return null;\n\n  return createPortal(\n    <AnimatePresence>\n      {ctx.open ? (\n        <motion.div\n          data-morph-popover-portal=\"\"\n          // Wrapper carries the shadow as a drop-shadow filter, which hugs the\n          // clipped shape below (box-shadow would just get clipped away).\n          variants={wrap}\n          initial={reduce ? { opacity: 0 } : \"hidden\"}\n          animate={reduce ? { opacity: 1 } : \"show\"}\n          exit={reduce ? { opacity: 0 } : \"hidden\"}\n          transition={reduce ? { duration: 0.12 } : undefined}\n          style={{\n            left,\n            top,\n            visibility: layout ? \"visible\" : \"hidden\",\n            transformOrigin: originFor(side, align),\n          }}\n          className=\"fixed z-[9999] [filter:drop-shadow(0_10px_18px_rgba(0,0,0,0.14))]\"\n        >\n          <motion.div\n            ref={ctx.contentRef}\n            id={ctx.contentId}\n            role=\"dialog\"\n            aria-labelledby={ctx.triggerId}\n            variants={clip}\n            style={{ borderRadius: radius }}\n            className={cn(\n              \"overflow-hidden border border-border bg-background\",\n              className,\n            )}\n          >\n            {children}\n          </motion.div>\n        </motion.div>\n      ) : null}\n    </AnimatePresence>,\n    document.body,\n  );\n}\n"},{"path":"components/agents/approval-card/types.ts","type":"registry:component","target":"@components/agents/approval-card/types.ts","content":"import type { ReactNode } from \"react\";\n\nexport type ApprovalCardStatus =\n  | \"pending\"\n  | \"submitting\"\n  | \"approved\"\n  | \"rejected\"\n  | \"changes-requested\"\n  | \"answered\";\n\nexport interface ApprovalCardOption {\n  value: string;\n  label: string;\n  disabled?: boolean;\n}\n\nexport interface ApprovalCardQuestion {\n  id: string;\n  title: ReactNode;\n  description?: ReactNode;\n  options?: ApprovalCardOption[];\n  multiple?: boolean;\n  autoAdvance?: boolean;\n  allowCustom?: boolean;\n  customPlaceholder?: string;\n}\n\nexport interface ApprovalCardAnswer {\n  selected: string[];\n  custom?: string;\n}\n\nexport type ApprovalCardAnswers = Record<string, ApprovalCardAnswer>;\n\nexport interface ApprovalCardProps {\n  title?: ReactNode;\n  description?: ReactNode;\n  children?: ReactNode;\n  questions?: ApprovalCardQuestion[];\n  status?: ApprovalCardStatus;\n  answers?: ApprovalCardAnswers;\n  defaultAnswers?: ApprovalCardAnswers;\n  onAnswersChange?: (answers: ApprovalCardAnswers) => void;\n  step?: number;\n  defaultStep?: number;\n  onStepChange?: (step: number) => void;\n  onSubmit?: (answers: ApprovalCardAnswers) => void;\n  onApprove?: () => void;\n  onReject?: () => void;\n  onRequestChanges?: () => void;\n  onDismiss?: () => void;\n  approveLabel?: ReactNode;\n  submitLabel?: ReactNode;\n  result?: ReactNode;\n  className?: string;\n}\n"},{"path":"components/motion/action-swap-roll.tsx","type":"registry:component","target":"@components/motion/action-swap-roll.tsx","content":"\"use client\";\n\nimport {\n  ActionSwapButton,\n  ActionSwapIcon,\n  ActionSwapText,\n  type ActionSwapButtonProps,\n  type ActionSwapIconProps,\n  type ActionSwapTextProps,\n} from \"./action-swap\";\n\nexport type {\n  ActionSwapButtonSize,\n  ActionSwapButtonVariant,\n  ActionSwapItem,\n} from \"./action-swap\";\n\nexport type ActionSwapRollButtonProps = Omit<ActionSwapButtonProps, \"animation\">;\nexport type ActionSwapRollTextProps = Omit<ActionSwapTextProps, \"animation\">;\nexport type ActionSwapRollIconProps = Omit<ActionSwapIconProps, \"animation\">;\n\nexport function ActionSwapRollButton(props: ActionSwapRollButtonProps) {\n  return <ActionSwapButton {...props} animation=\"roll\" />;\n}\n\nexport function ActionSwapRollText(props: ActionSwapRollTextProps) {\n  return <ActionSwapText {...props} animation=\"roll\" />;\n}\n\nexport function ActionSwapRollIcon(props: ActionSwapRollIconProps) {\n  return <ActionSwapIcon {...props} animation=\"roll\" />;\n}\n"},{"path":"components/motion/button/index.tsx","type":"registry:component","target":"@components/motion/button/index.tsx","content":"export { Button } from \"./base\";\nexport type { ButtonProps, ButtonVariant, ButtonSize } from \"./base\";\n\nexport { StatefulButton } from \"./stateful\";\nexport type { StatefulButtonProps, ButtonState } from \"./stateful\";\n\nexport { MagneticButton } from \"./magnetic\";\nexport type { MagneticButtonProps } from \"./magnetic\";\n"},{"path":"components/motion/checkbox.tsx","type":"registry:component","target":"@components/motion/checkbox.tsx","content":"\"use client\";\n\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useId } from \"react\";\nimport { EASE_OUT, SPRING_PRESS } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nconst CHECK_PATH = \"M5 13l4 4L19 7\";\nconst INDETERMINATE_PATH = \"M6 12h12\";\n\nexport interface CheckboxProps {\n  checked: boolean;\n  onCheckedChange: (checked: boolean) => void;\n  disabled?: boolean;\n  indeterminate?: boolean;\n  label?: string;\n  className?: string;\n  id?: string;\n  \"aria-label\"?: string;\n}\n\nexport function Checkbox({\n  checked,\n  onCheckedChange,\n  disabled,\n  indeterminate,\n  label,\n  className,\n  id: idProp,\n  \"aria-label\": ariaLabel,\n}: CheckboxProps) {\n  const autoId = useId();\n  const id = idProp ?? autoId;\n  const reduce = useReducedMotion();\n  const showMark = checked || indeterminate;\n  const path = indeterminate ? INDETERMINATE_PATH : CHECK_PATH;\n\n  return (\n    <label\n      htmlFor={id}\n      className={cn(\n        \"inline-flex items-center gap-3\",\n        disabled ? \"cursor-not-allowed\" : \"cursor-pointer\",\n        className,\n      )}\n    >\n      <motion.button\n        id={id}\n        type=\"button\"\n        role=\"checkbox\"\n        aria-checked={indeterminate ? \"mixed\" : checked}\n        aria-label={ariaLabel}\n        disabled={disabled}\n        onClick={() => !disabled && onCheckedChange(!checked)}\n        whileTap={reduce || disabled ? undefined : { scale: 0.92 }}\n        transition={SPRING_PRESS}\n        data-state={\n          checked ? \"checked\" : indeterminate ? \"indeterminate\" : \"unchecked\"\n        }\n        className={cn(\n          \"inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-md border-2 outline-none transition-colors duration-200\",\n          \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n          \"disabled:cursor-not-allowed disabled:opacity-60\",\n          showMark\n            ? \"border-primary bg-primary text-primary-foreground\"\n            : \"border-muted-foreground/50 bg-background hover:border-muted-foreground\",\n        )}\n      >\n        <AnimatePresence initial={false}>\n          {showMark ? (\n            <motion.svg\n              key={indeterminate ? \"indeterminate\" : \"checked\"}\n              width=\"12\"\n              height=\"12\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              strokeWidth={3}\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              initial={reduce ? { opacity: 1 } : { opacity: 0, scale: 0.5 }}\n              animate={reduce ? { opacity: 1 } : { opacity: 1, scale: 1 }}\n              exit={\n                reduce\n                  ? { opacity: 0 }\n                  : { opacity: 0, scale: 0.5, filter: \"blur(4px)\" }\n              }\n              transition={\n                reduce ? { duration: 0 } : { duration: 0.16, ease: EASE_OUT }\n              }\n              aria-hidden\n            >\n              <title>{indeterminate ? \"Partially selected\" : \"Selected\"}</title>\n              <motion.path\n                d={path}\n                initial={reduce ? { pathLength: 1 } : { pathLength: 0 }}\n                animate={{ pathLength: 1 }}\n                transition={\n                  reduce\n                    ? { duration: 0 }\n                    : {\n                        duration: indeterminate ? 0.2 : 0.3,\n                        ease: EASE_OUT,\n                        delay: 0.04,\n                      }\n                }\n              />\n            </motion.svg>\n          ) : null}\n        </AnimatePresence>\n      </motion.button>\n      {label ? (\n        <span className={cn(\"select-none text-sm text-foreground\", disabled && \"opacity-60\")}>\n          {label}\n        </span>\n      ) : null}\n    </label>\n  );\n}\n"},{"path":"components/motion/input.tsx","type":"registry:component","target":"@components/motion/input.tsx","content":"\"use client\";\n\nimport {\n  AnimatePresence,\n  animate,\n  motion,\n  useReducedMotion,\n} from \"motion/react\";\nimport {\n  forwardRef,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n  type InputHTMLAttributes,\n  type ReactNode,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type InputClassNames = {\n  root?: string;\n  label?: string;\n  field?: string;\n  input?: string;\n  leftIcon?: string;\n  rightIcon?: string;\n  successIcon?: string;\n  errorMessage?: string;\n};\n\nexport interface InputProps extends Omit<\n  InputHTMLAttributes<HTMLInputElement>,\n  \"value\" | \"defaultValue\" | \"onChange\"\n> {\n  label?: string;\n  value?: string;\n  defaultValue?: string;\n  onChange?: (value: string) => void;\n  /** Truthy error triggers a shake, red border and (if a string) a message. */\n  error?: string | boolean;\n  success?: boolean;\n  leftIcon?: ReactNode;\n  rightIcon?: ReactNode;\n  className?: string;\n  classNames?: InputClassNames;\n}\n\nexport const Input = forwardRef<HTMLInputElement, InputProps>(function Input(\n  {\n    label,\n    value: valueProp,\n    defaultValue,\n    onChange,\n    onFocus,\n    onBlur,\n    error,\n    success,\n    leftIcon,\n    rightIcon,\n    className,\n    classNames,\n    disabled,\n    id: idProp,\n    type,\n    ...rest\n  },\n  ref,\n) {\n  const reactId = useId();\n  const id = idProp ?? reactId;\n  const reduce = useReducedMotion();\n\n  const controlled = valueProp !== undefined;\n  const [internal, setInternal] = useState(defaultValue ?? \"\");\n  const value = controlled ? (valueProp ?? \"\") : internal;\n\n  const [focused, setFocused] = useState(false);\n\n  const fieldRef = useRef<HTMLDivElement>(null);\n\n  const hasError = Boolean(error);\n  const errorMessage = typeof error === \"string\" ? error : null;\n\n  // Right edge shows the success check, otherwise the caller's right icon.\n  const rightSlot = success ? null : rightIcon;\n\n  // Shake the field when an error appears.\n  useEffect(() => {\n    if (!fieldRef.current || reduce || !hasError) return;\n    animate(\n      fieldRef.current,\n      { x: [0, -6, 6, -4, 4, -2, 0] },\n      { duration: 0.45 },\n    );\n  }, [hasError, reduce]);\n\n  const handleChange = (next: string) => {\n    if (!controlled) setInternal(next);\n    onChange?.(next);\n  };\n\n  return (\n    <div\n      className={cn(\"flex flex-col gap-1.5\", className, classNames?.root)}\n    >\n      {label ? (\n        <label\n          htmlFor={id}\n          className={cn(\n            \"px-1 text-sm font-medium text-foreground\",\n            classNames?.label,\n          )}\n        >\n          {label}\n        </label>\n      ) : null}\n\n      <div\n        ref={fieldRef}\n        data-state={\n          hasError\n            ? \"error\"\n            : success\n              ? \"success\"\n              : focused\n                ? \"focused\"\n                : \"idle\"\n        }\n        className={cn(\n          \"relative h-11 overflow-hidden rounded-full border transition-colors duration-200\",\n          \"border-border\",\n          focused && !hasError && \"border-foreground/40 ring-2 ring-ring/40\",\n          hasError && \"border-destructive ring-2 ring-destructive/25\",\n          disabled && \"opacity-60\",\n          classNames?.field,\n        )}\n      >\n        {leftIcon ? (\n          <span\n            className={cn(\n              \"pointer-events-none absolute left-3 top-1/2 flex -translate-y-1/2 items-center text-muted-foreground [&_svg]:h-4 [&_svg]:w-4\",\n              classNames?.leftIcon,\n            )}\n          >\n            {leftIcon}\n          </span>\n        ) : null}\n\n        <input\n          ref={ref}\n          id={id}\n          type={type}\n          value={value}\n          disabled={disabled}\n          aria-invalid={hasError || undefined}\n          aria-describedby={errorMessage ? `${id}-error` : undefined}\n          {...rest}\n          onChange={(e) => handleChange(e.target.value)}\n          onFocus={(event) => {\n            setFocused(true);\n            onFocus?.(event);\n          }}\n          onBlur={(event) => {\n            setFocused(false);\n            onBlur?.(event);\n          }}\n          className={cn(\n            \"peer h-full w-full bg-transparent text-base leading-6 text-foreground caret-foreground outline-none\",\n            \"placeholder:text-muted-foreground/60\",\n            leftIcon ? \"pl-10\" : \"pl-3.5\",\n            rightSlot || success ? \"pr-10\" : \"pr-3.5\",\n            disabled && \"cursor-not-allowed\",\n            classNames?.input,\n          )}\n        />\n\n        {success ? (\n          <motion.svg\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            className={cn(\n              \"absolute right-3.5 top-1/2 h-5 w-5 -translate-y-1/2 text-(--color-success)\",\n              classNames?.successIcon,\n            )}\n          >\n            <motion.path\n              d=\"M5 12.5l4.5 4.5L19 7.5\"\n              stroke=\"currentColor\"\n              strokeWidth={2.5}\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              initial={reduce ? { pathLength: 1 } : { pathLength: 0 }}\n              animate={{ pathLength: 1 }}\n              transition={{ duration: 0.35, ease: \"easeOut\" }}\n            />\n          </motion.svg>\n        ) : rightSlot ? (\n          <span\n            className={cn(\n              \"absolute right-0 top-0 flex h-full items-center text-muted-foreground [&_button]:grid [&_button]:size-11 [&_button]:place-items-center [&_svg]:h-4 [&_svg]:w-4\",\n              classNames?.rightIcon,\n            )}\n          >\n            {rightSlot}\n          </span>\n        ) : null}\n      </div>\n\n      <AnimatePresence initial={false}>\n        {errorMessage ? (\n          <motion.p\n            id={`${id}-error`}\n            role=\"alert\"\n            initial={\n              reduce\n                ? { opacity: 0 }\n                : { opacity: 0, y: -4, filter: \"blur(4px)\" }\n            }\n            animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n            exit={\n              reduce\n                ? { opacity: 0 }\n                : { opacity: 0, y: -4, filter: \"blur(4px)\" }\n            }\n            transition={{ duration: 0.2 }}\n            className={cn(\n              \"px-1 text-xs text-destructive\",\n              classNames?.errorMessage,\n            )}\n          >\n            {errorMessage}\n          </motion.p>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  );\n});\n"},{"path":"components/motion/radio.tsx","type":"registry:component","target":"@components/motion/radio.tsx","content":"\"use client\";\n\nimport { motion, MotionConfig, useReducedMotion } from \"motion/react\";\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useId,\n  useMemo,\n  useState,\n  type ReactNode,\n} from \"react\";\nimport { SPRING_LAYOUT, SPRING_PRESS } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\ntype RadioCtx = {\n  value: string;\n  setValue: (value: string) => void;\n  layoutId: string;\n};\n\nconst RadioCtx = createContext<RadioCtx | null>(null);\n\nfunction useRadioGroup() {\n  const ctx = useContext(RadioCtx);\n  if (!ctx) {\n    throw new Error(\"RadioGroupItem must be used inside <RadioGroup>\");\n  }\n  return ctx;\n}\n\nexport interface RadioGroupProps {\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  children: ReactNode;\n  className?: string;\n  orientation?: \"vertical\" | \"horizontal\";\n}\n\nexport function RadioGroup({\n  value,\n  defaultValue = \"\",\n  onValueChange,\n  children,\n  className,\n  orientation = \"vertical\",\n}: RadioGroupProps) {\n  const [internal, setInternal] = useState(defaultValue);\n  const layoutId = useId();\n  const reduce = useReducedMotion();\n  const controlled = value !== undefined;\n  const current = controlled ? value : internal;\n  const setValue = useCallback(\n    (next: string) => {\n      if (!controlled) setInternal(next);\n      onValueChange?.(next);\n    },\n    [controlled, onValueChange],\n  );\n  const contextValue = useMemo(\n    () => ({ value: current, setValue, layoutId }),\n    [current, layoutId, setValue],\n  );\n\n  return (\n    <MotionConfig transition={reduce ? { duration: 0 } : SPRING_LAYOUT}>\n      <RadioCtx.Provider value={contextValue}>\n        <div\n          role=\"radiogroup\"\n          className={cn(\n            \"flex gap-3\",\n            orientation === \"vertical\" ? \"flex-col\" : \"flex-row flex-wrap\",\n            className,\n          )}\n        >\n          {children}\n        </div>\n      </RadioCtx.Provider>\n    </MotionConfig>\n  );\n}\n\nexport interface RadioGroupItemProps {\n  value: string;\n  label?: string;\n  disabled?: boolean;\n  className?: string;\n  id?: string;\n}\n\nexport function RadioGroupItem({\n  value,\n  label,\n  disabled,\n  className,\n  id: idProp,\n}: RadioGroupItemProps) {\n  const { value: groupValue, setValue, layoutId } = useRadioGroup();\n  const autoId = useId();\n  const id = idProp ?? autoId;\n  const reduce = useReducedMotion();\n  const selected = groupValue === value;\n\n  return (\n    <label\n      htmlFor={id}\n      className={cn(\n        \"inline-flex items-center gap-3\",\n        disabled ? \"cursor-not-allowed\" : \"cursor-pointer\",\n        className,\n      )}\n    >\n      <motion.button\n        id={id}\n        type=\"button\"\n        role=\"radio\"\n        aria-checked={selected}\n        disabled={disabled}\n        onClick={() => !disabled && setValue(value)}\n        whileTap={reduce || disabled ? undefined : { scale: 0.92 }}\n        transition={SPRING_PRESS}\n        data-state={selected ? \"checked\" : \"unchecked\"}\n        className={cn(\n          \"relative inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full border-2 outline-none transition-colors duration-200\",\n          \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n          \"disabled:cursor-not-allowed disabled:opacity-60\",\n          selected\n            ? \"border-primary\"\n            : \"border-muted-foreground/50 hover:border-muted-foreground\",\n        )}\n      >\n        {selected ? (\n          <motion.span\n            layoutId={layoutId}\n            className=\"absolute inset-1 rounded-full bg-primary\"\n            transition={reduce ? { duration: 0 } : SPRING_LAYOUT}\n          />\n        ) : null}\n      </motion.button>\n      {label ? (\n        <span className={cn(\"select-none text-sm text-foreground\", disabled && \"opacity-60\")}>\n          {label}\n        </span>\n      ) : null}\n    </label>\n  );\n}\n"},{"path":"components/agents/agent-code.tsx","type":"registry:component","target":"@components/agents/agent-code.tsx","content":"\"use client\";\n\nimport {\n  type CSSProperties,\n  Fragment,\n  useEffect,\n  useState,\n} from \"react\";\nimport { createHighlighter, type Highlighter } from \"shiki\";\nimport { cn } from \"@/lib/utils\";\n\nexport type AgentCodeLanguage =\n  | \"bash\"\n  | \"diff\"\n  | \"json\"\n  | \"text\"\n  | \"tsx\"\n  | \"typescript\";\n\nexport interface AgentCodeToken {\n  content: string;\n  offset: number;\n  light?: string;\n  dark?: string;\n}\n\nexport type AgentCodeTokenLines = AgentCodeToken[][];\n\nexport interface AgentCodeProps {\n  code: string;\n  language?: AgentCodeLanguage;\n  className?: string;\n}\n\nexport interface AgentCodeLineProps {\n  code: string;\n  tokens?: AgentCodeToken[];\n  className?: string;\n}\n\nconst LIGHT_THEME = \"github-light-high-contrast\";\nconst DARK_THEME = \"github-dark-high-contrast\";\nlet agentCodeHighlighter: Promise<Highlighter> | null = null;\nconst tokenCache = new Map<string, AgentCodeTokenLines>();\n\nfunction getAgentCodeHighlighter() {\n  if (!agentCodeHighlighter) {\n    agentCodeHighlighter = createHighlighter({\n      themes: [LIGHT_THEME, DARK_THEME],\n      langs: [\"bash\", \"diff\", \"json\", \"tsx\", \"typescript\"],\n    });\n  }\n  return agentCodeHighlighter;\n}\n\nfunction tokenCacheKey(code: string, language: AgentCodeLanguage) {\n  return `${language}\\u0000${code}`;\n}\n\nexport function useAgentCodeTokens(\n  code: string,\n  language: AgentCodeLanguage,\n) {\n  const key = tokenCacheKey(code, language);\n  const cached = tokenCache.get(key);\n  const [result, setResult] = useState<{\n    key: string;\n    code: string;\n    language: AgentCodeLanguage;\n    lines: AgentCodeTokenLines;\n  } | null>(cached ? { key, code, language, lines: cached } : null);\n\n  useEffect(() => {\n    const current = tokenCache.get(key);\n    if (current) {\n      setResult({ key, code, language, lines: current });\n      return;\n    }\n\n    let cancelled = false;\n    getAgentCodeHighlighter().then((highlighter) => {\n      if (cancelled) return;\n      const lines = highlighter\n        .codeToTokensWithThemes(code, {\n          lang: language,\n          themes: {\n            light: LIGHT_THEME,\n            dark: DARK_THEME,\n          },\n        })\n        .map((line) =>\n          line.map((token) => ({\n            content: token.content,\n            offset: token.offset,\n            light: token.variants.light?.color,\n            dark: token.variants.dark?.color,\n          })),\n      );\n      tokenCache.set(key, lines);\n      setResult({ key, code, language, lines });\n    });\n    return () => {\n      cancelled = true;\n    };\n  }, [code, key, language]);\n\n  if (result?.key === key) return result.lines;\n  if (result?.language === language && code.startsWith(result.code)) {\n    return result.lines;\n  }\n  return null;\n}\n\nexport function AgentCodeLine({\n  code,\n  tokens,\n  className,\n}: AgentCodeLineProps) {\n  return (\n    <span className={className}>\n      {tokens\n        ? tokens.map((token) => (\n            <span\n              key={`${token.offset}-${token.content}`}\n              style={\n                {\n                  \"--agent-code-light\": token.light ?? \"currentColor\",\n                  \"--agent-code-dark\": token.dark ?? token.light ?? \"currentColor\",\n                } as CSSProperties\n              }\n              className=\"text-[var(--agent-code-light)] dark:text-[var(--agent-code-dark)]\"\n            >\n              {token.content}\n            </span>\n          ))\n        : code}\n    </span>\n  );\n}\n\nexport function AgentCode({\n  code,\n  language = \"bash\",\n  className,\n}: AgentCodeProps) {\n  const tokens = useAgentCodeTokens(code, language);\n  let offset = 0;\n  const lines = code.split(\"\\n\").map((content) => {\n    const line = { content, offset };\n    offset += content.length + 1;\n    return line;\n  });\n\n  return (\n    <pre\n      className={cn(\n        \"m-0 overflow-x-auto whitespace-pre font-mono text-xs leading-5 text-foreground/85\",\n        className,\n      )}\n    >\n      <code>\n        {lines.map((line, index) => (\n          <Fragment key={line.offset}>\n            <AgentCodeLine code={line.content} tokens={tokens?.[index]} />\n            {index < lines.length - 1 ? \"\\n\" : null}\n          </Fragment>\n        ))}\n      </code>\n    </pre>\n  );\n}\n"},{"path":"lib/hooks/use-hover-capable.ts","type":"registry:hook","target":"@lib/hooks/use-hover-capable.ts","content":"\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\n/**\n * Returns true only on devices that have a true hover (mouse / trackpad).\n * Touch devices fire phantom `:hover` on tap that sticks until tap-elsewhere\n * — gate hover-only effects (scale lifts, magnetic pulls) behind this.\n */\nexport function useHoverCapable() {\n  const [canHover, setCanHover] = useState(false);\n\n  useEffect(() => {\n    if (typeof window === \"undefined\" || !window.matchMedia) return;\n    const mq = window.matchMedia(\"(hover: hover) and (pointer: fine)\");\n    const update = () => setCanHover(mq.matches);\n    update();\n    mq.addEventListener?.(\"change\", update);\n    return () => mq.removeEventListener?.(\"change\", update);\n  }, []);\n\n  return canHover;\n}\n"},{"path":"components/motion/text-shimmer.tsx","type":"registry:component","target":"@components/motion/text-shimmer.tsx","content":"import { cn } from \"@/lib/utils\";\nimport type { ElementType, ReactNode } from \"react\";\nimport {\n  TEXT_SHIMMER_CLASS_NAME,\n  TEXT_SHIMMER_KEYFRAMES,\n  textShimmerStyle,\n} from \"@/lib/text-shimmer\";\n\nexport interface TextShimmerProps {\n  children: ReactNode;\n  as?: ElementType;\n  duration?: number;\n  className?: string;\n}\n\nexport function TextShimmer({ children, as: Comp = \"span\", duration = 2.5, className }: TextShimmerProps) {\n  return (\n    <>\n      <style>\n        {TEXT_SHIMMER_KEYFRAMES}\n      </style>\n      <Comp\n        style={textShimmerStyle(duration)}\n        className={cn(\n          \"inline-block\",\n          TEXT_SHIMMER_CLASS_NAME,\n          className,\n        )}\n      >\n        {children}\n      </Comp>\n    </>\n  );\n}\n"},{"path":"components/agents/message-context.tsx","type":"registry:component","target":"@components/agents/message-context.tsx","content":"\"use client\";\n\nimport { createContext } from \"react\";\n\nexport type MessageSide = \"start\" | \"end\";\n\nexport const MessageSideContext = createContext<MessageSide | undefined>(\n  undefined,\n);\n"},{"path":"components/motion/preview-rail.tsx","type":"registry:component","target":"@components/motion/preview-rail.tsx","content":"\"use client\";\n\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useId, useState, type ReactNode } from \"react\";\nimport { EASE_OUT, SPRING_LAYOUT } from \"@/lib/ease\";\nimport { useHoverCapable } from \"@/lib/hooks/use-hover-capable\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface PreviewRailItem {\n  id: string;\n  label: string;\n  ariaLabel?: string;\n  description?: ReactNode;\n  href?: string;\n  target?: \"_blank\" | \"_self\" | \"_parent\" | \"_top\";\n  rel?: string;\n}\n\nexport interface PreviewRailProps {\n  items: PreviewRailItem[];\n  label?: string;\n  orientation?: \"vertical\" | \"horizontal\";\n  activeId?: string;\n  defaultActiveId?: string;\n  onActiveChange?: (id: string) => void;\n  onItemSelect?: (item: PreviewRailItem) => void;\n  renderPreview?: (item: PreviewRailItem) => ReactNode;\n  showPreview?: boolean;\n  previewSide?: \"before\" | \"after\";\n  highlightActive?: boolean;\n  itemSize?: number;\n  children?: ReactNode;\n  className?: string;\n  railClassName?: string;\n  previewContainerClassName?: string;\n  previewClassName?: string;\n}\n\nfunction DefaultPreview({ item }: { item: PreviewRailItem }) {\n  return (\n    <div\n      data-slot=\"preview-rail-card\"\n      className=\"rounded-2xl border border-border bg-card p-4 shadow-sm\"\n    >\n      <p\n        data-slot=\"preview-rail-title\"\n        className=\"font-medium text-card-foreground\"\n      >\n        {item.label}\n      </p>\n      {item.description ? (\n        <div\n          data-slot=\"preview-rail-description\"\n          className=\"mt-1 text-sm leading-6 text-muted-foreground\"\n        >\n          {item.description}\n        </div>\n      ) : null}\n    </div>\n  );\n}\n\nexport function PreviewRail({\n  items,\n  label = \"Section navigation\",\n  orientation = \"vertical\",\n  activeId,\n  defaultActiveId,\n  onActiveChange,\n  onItemSelect,\n  renderPreview,\n  showPreview = true,\n  previewSide = \"after\",\n  highlightActive = false,\n  itemSize = 24,\n  children,\n  className,\n  railClassName,\n  previewContainerClassName,\n  previewClassName,\n}: PreviewRailProps) {\n  const uid = useId();\n  const reduce = useReducedMotion();\n  const canHover = useHoverCapable();\n  const [internalActiveId, setInternalActiveId] = useState(\n    defaultActiveId ?? items[0]?.id ?? \"\",\n  );\n  const [hoveredId, setHoveredId] = useState<string | null>(null);\n  const [focusedId, setFocusedId] = useState<string | null>(null);\n\n  const requestedActiveId = activeId ?? internalActiveId;\n  const selectedId = items.some((item) => item.id === requestedActiveId)\n    ? requestedActiveId\n    : (items[0]?.id ?? \"\");\n  const displayedId = hoveredId ?? focusedId ?? \"\";\n  const highlightedId = displayedId || (highlightActive ? selectedId : \"\");\n  const displayedIndex = items.findIndex((item) => item.id === highlightedId);\n  const rowTemplate = items.length\n    ? `repeat(${items.length}, ${itemSize}px)`\n    : undefined;\n  const isHorizontal = orientation === \"horizontal\";\n\n  const selectItem = (id: string) => {\n    if (activeId === undefined) setInternalActiveId(id);\n    onActiveChange?.(id);\n  };\n\n  return (\n    <motion.div\n      layoutRoot\n      onBlur={(event) => {\n        if (!event.currentTarget.contains(event.relatedTarget)) {\n          setFocusedId(null);\n        }\n      }}\n      className={cn(\n        \"isolate relative flex w-full overflow-visible\",\n        isHorizontal\n          ? \"min-h-64 flex-col items-center justify-center\"\n          : \"min-h-80\",\n        className,\n      )}\n    >\n      <nav\n        aria-label={label}\n        onPointerLeave={() => setHoveredId(null)}\n        style={\n          isHorizontal\n            ? { gridTemplateColumns: rowTemplate }\n            : { gridTemplateRows: rowTemplate }\n        }\n        className={cn(\n          \"relative z-10 grid shrink-0\",\n          isHorizontal\n            ? \"h-12 w-fit max-w-full self-center justify-center\"\n            : \"w-12 content-center\",\n          railClassName,\n        )}\n      >\n        {items.map((item, index) => {\n          const selected = item.id === selectedId;\n          const highlighted = item.id === highlightedId;\n          const distance =\n            displayedIndex < 0 ? Number.POSITIVE_INFINITY : Math.abs(index - displayedIndex);\n          const scale = highlighted\n            ? 1\n            : distance === 1\n              ? 0.68\n              : distance === 2\n                ? 0.44\n                : 0.25;\n\n          const itemContent = (\n            <>\n              <motion.span\n                data-slot=\"preview-rail-tick\"\n                aria-hidden=\"true\"\n                animate={isHorizontal ? { scaleY: scale } : { scaleX: scale }}\n                transition={reduce ? { duration: 0 } : SPRING_LAYOUT}\n                className={cn(\n                  \"block bg-current\",\n                  isHorizontal\n                    ? \"h-12 w-0.5 origin-bottom\"\n                    : \"h-0.5 w-12 origin-left\",\n                  highlighted ? \"text-foreground\" : undefined,\n                )}\n              />\n            </>\n          );\n\n          const sharedClassName = cn(\n            \"relative flex text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n            isHorizontal\n              ? \"h-12 w-6 items-end justify-center\"\n              : \"h-6 w-12 items-center\",\n          );\n          const sharedStyle = isHorizontal\n            ? { width: itemSize }\n            : { height: itemSize };\n          const handlePointerEnter = () => {\n            if (canHover) setHoveredId(item.id);\n          };\n          const handleFocus = (currentTarget: HTMLElement) => {\n            if (currentTarget.matches(\":focus-visible\")) {\n              setFocusedId(item.id);\n            }\n          };\n          const handleSelect = () => {\n            selectItem(item.id);\n            onItemSelect?.(item);\n          };\n\n          return item.href ? (\n            <a\n              key={item.id}\n              data-slot=\"preview-rail-item\"\n              href={item.href}\n              target={item.target}\n              rel={\n                item.rel ??\n                (item.target === \"_blank\" ? \"noreferrer noopener\" : undefined)\n              }\n              aria-label={item.ariaLabel ?? item.label}\n              aria-current={selected ? \"page\" : undefined}\n              onPointerEnter={handlePointerEnter}\n              onMouseEnter={handlePointerEnter}\n              onPointerDown={() => setFocusedId(null)}\n              onFocus={(event) => handleFocus(event.currentTarget)}\n              onClick={handleSelect}\n              style={sharedStyle}\n              className={sharedClassName}\n            >\n              {itemContent}\n            </a>\n          ) : (\n            <button\n              key={item.id}\n              data-slot=\"preview-rail-item\"\n              type=\"button\"\n              aria-label={item.ariaLabel ?? item.label}\n              aria-current={selected ? \"location\" : undefined}\n              onPointerEnter={handlePointerEnter}\n              onMouseEnter={handlePointerEnter}\n              onPointerDown={() => setFocusedId(null)}\n              onFocus={(event) => handleFocus(event.currentTarget)}\n              onClick={handleSelect}\n              style={sharedStyle}\n              className={sharedClassName}\n            >\n              {itemContent}\n            </button>\n          );\n        })}\n      </nav>\n\n      {showPreview ? (\n        <div\n          aria-hidden=\"true\"\n          style={\n            isHorizontal\n              ? { gridTemplateColumns: rowTemplate }\n              : { gridTemplateRows: rowTemplate }\n          }\n          className={cn(\n            \"pointer-events-none absolute z-50 grid\",\n            isHorizontal\n              ? \"top-1/2 left-1/2 h-5 w-fit max-w-full -translate-x-1/2 -translate-y-1/2 justify-center\"\n              : previewSide === \"before\"\n                ? \"inset-y-0 right-16 left-4 content-center\"\n                : \"inset-y-0 right-4 left-16 content-center\",\n            previewContainerClassName,\n          )}\n        >\n          {items.map((item) => (\n            <div\n              key={item.id}\n              style={\n                isHorizontal ? { width: itemSize } : { height: itemSize }\n              }\n              className={cn(\n                \"relative flex items-center\",\n                isHorizontal ? \"justify-center\" : undefined,\n              )}\n            >\n              {item.id === displayedId ? (\n                <div\n                  className={cn(\n                    isHorizontal\n                      ? \"absolute bottom-12 left-1/2 w-72 -translate-x-1/2\"\n                      : cn(\n                          \"w-full max-w-sm\",\n                          previewSide === \"before\" && \"ml-auto\",\n                        ),\n                    previewClassName,\n                  )}\n                >\n                  <motion.div\n                    layoutId={`preview-rail-card-${uid}`}\n                    transition={reduce ? { duration: 0 } : SPRING_LAYOUT}\n                  >\n                    <AnimatePresence mode=\"wait\" initial={false}>\n                      <motion.div\n                        key={item.id}\n                        initial={\n                          reduce\n                            ? { opacity: 0 }\n                            : { opacity: 0, y: 4, filter: \"blur(6px)\" }\n                        }\n                        animate={\n                          reduce\n                            ? { opacity: 1 }\n                            : { opacity: 1, y: 0, filter: \"blur(0px)\" }\n                        }\n                        exit={\n                          reduce\n                            ? { opacity: 0 }\n                            : {\n                                opacity: 0,\n                                y: -2,\n                                filter: \"blur(4px)\",\n                                transition: {\n                                  duration: 0.12,\n                                  ease: EASE_OUT,\n                                },\n                              }\n                        }\n                        transition={{\n                          duration: reduce ? 0 : 0.18,\n                          ease: EASE_OUT,\n                        }}\n                      >\n                        {renderPreview ? (\n                          renderPreview(item)\n                        ) : (\n                          <DefaultPreview item={item} />\n                        )}\n                      </motion.div>\n                    </AnimatePresence>\n                  </motion.div>\n                </div>\n              ) : null}\n            </div>\n          ))}\n        </div>\n      ) : null}\n\n      {children ? (\n        <div className=\"min-h-0 min-w-0 flex-1\">{children}</div>\n      ) : null}\n    </motion.div>\n  );\n}\n"},{"path":"components/motion/select.tsx","type":"registry:component","target":"@components/motion/select.tsx","content":"\"use client\";\n\nimport { Check, ChevronDown } from \"lucide-react\";\nimport {\n  motion,\n  type Transition,\n  useReducedMotion,\n  type Variants,\n} from \"motion/react\";\nimport {\n  createContext,\n  type ReactNode,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { EASE_OUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nconst INSTANT_TRANSITION: Transition = { duration: 0 };\n\n// Spring with bounce powers the unfold/separation; per-property timings in the\n// content choreograph it (see SelectContent). Mirrors bouncy-accordion's feel.\nconst CHEVRON_TRANSITION: Transition = { type: \"spring\", duration: 0.4, bounce: 0.3 };\n\nconst LIST_VARIANTS: Variants = {\n  hidden: {},\n  show: { transition: { staggerChildren: 0.035, delayChildren: 0.05 } },\n};\nconst ITEM_VARIANTS: Variants = {\n  hidden: { opacity: 0, y: -6, filter: \"blur(3px)\" },\n  show: { opacity: 1, y: 0, filter: \"blur(0px)\" },\n};\n\ntype Placement = \"bottom\" | \"top\";\n\ninterface SelectContextValue {\n  value: string | undefined;\n  open: boolean;\n  setOpen: (open: boolean) => void;\n  select: (value: string) => void;\n  register: (value: string, label: string) => void;\n  unregister: (value: string) => void;\n  labelFor: (value: string | undefined) => string | undefined;\n  reduce: boolean;\n  triggerId: string;\n  listId: string;\n  disabled: boolean;\n  placement: Placement;\n  setPlacement: (p: Placement) => void;\n}\n\nconst SelectContext = createContext<SelectContextValue | null>(null);\n\nfunction useSelectContext(component: string) {\n  const ctx = useContext(SelectContext);\n  if (!ctx) throw new Error(`${component} must be used within <Select>`);\n  return ctx;\n}\n\nexport interface SelectProps {\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  disabled?: boolean;\n  className?: string;\n  children: ReactNode;\n}\n\nexport function Select({\n  value,\n  defaultValue,\n  onValueChange,\n  disabled = false,\n  className,\n  children,\n}: SelectProps) {\n  const reduce = useReducedMotion() ?? false;\n  const baseId = useId();\n  const rootRef = useRef<HTMLDivElement>(null);\n  const [open, setOpen] = useState(false);\n  const [internal, setInternal] = useState(defaultValue);\n  const [labels, setLabels] = useState<Map<string, string>>(new Map());\n  const [placement, setPlacement] = useState<Placement>(\"bottom\");\n\n  const controlled = value !== undefined;\n  const current = controlled ? value : internal;\n\n  const select = useCallback(\n    (next: string) => {\n      if (!controlled) setInternal(next);\n      onValueChange?.(next);\n      setOpen(false);\n    },\n    [controlled, onValueChange],\n  );\n\n  const register = useCallback((v: string, label: string) => {\n    setLabels((m) => (m.get(v) === label ? m : new Map(m).set(v, label)));\n  }, []);\n  const unregister = useCallback((v: string) => {\n    setLabels((m) => {\n      if (!m.has(v)) return m;\n      const next = new Map(m);\n      next.delete(v);\n      return next;\n    });\n  }, []);\n\n  // close on outside pointer / escape\n  useEffect(() => {\n    if (!open) return;\n    const onKey = (e: KeyboardEvent) => e.key === \"Escape\" && setOpen(false);\n    const onPointer = (e: PointerEvent) => {\n      if (rootRef.current && !rootRef.current.contains(e.target as Node))\n        setOpen(false);\n    };\n    window.addEventListener(\"keydown\", onKey);\n    window.addEventListener(\"pointerdown\", onPointer);\n    return () => {\n      window.removeEventListener(\"keydown\", onKey);\n      window.removeEventListener(\"pointerdown\", onPointer);\n    };\n  }, [open]);\n\n  const ctx = useMemo<SelectContextValue>(\n    () => ({\n      value: current,\n      open,\n      setOpen,\n      select,\n      register,\n      unregister,\n      labelFor: (v) => (v === undefined ? undefined : labels.get(v)),\n      reduce,\n      triggerId: `${baseId}-trigger`,\n      listId: `${baseId}-list`,\n      disabled,\n      placement,\n      setPlacement,\n    }),\n    [\n      current,\n      open,\n      select,\n      register,\n      unregister,\n      labels,\n      reduce,\n      baseId,\n      disabled,\n      placement,\n    ],\n  );\n\n  return (\n    <SelectContext.Provider value={ctx}>\n      <div ref={rootRef} className={cn(\"relative\", className)}>\n        {children}\n      </div>\n    </SelectContext.Provider>\n  );\n}\n\nexport interface SelectTriggerProps {\n  className?: string;\n  children: ReactNode;\n}\n\nexport function SelectTrigger({ className, children }: SelectTriggerProps) {\n  const ctx = useSelectContext(\"SelectTrigger\");\n  const isTop = ctx.placement === \"top\";\n  // edge facing the panel flattens then rounds; the far edge stays rounded.\n  // All four corners are specified so none gets stranded when placement flips.\n  const kf = ctx.open ? [0, 0, 12] : [12, 0, 12];\n  const kfT: Transition = ctx.reduce\n    ? { duration: 0 }\n    : ctx.open\n      ? { duration: 0.6, times: [0, 0.4, 1], ease: EASE_OUT }\n      : { duration: 0.42, times: [0, 0.5, 1], ease: EASE_OUT };\n  return (\n    <motion.button\n      type=\"button\"\n      id={ctx.triggerId}\n      disabled={ctx.disabled}\n      aria-haspopup=\"listbox\"\n      aria-expanded={ctx.open}\n      aria-controls={ctx.listId}\n      onClick={() => ctx.setOpen(!ctx.open)}\n      // Gooey: the edge facing the panel snaps flat (panel attached) then rounds\n      // back once the panel pulls away — the two pinch apart.\n      initial={false}\n      animate={{\n        borderTopLeftRadius: isTop ? kf : 12,\n        borderTopRightRadius: isTop ? kf : 12,\n        borderBottomLeftRadius: isTop ? 12 : kf,\n        borderBottomRightRadius: isTop ? 12 : kf,\n      }}\n      transition={{\n        borderTopLeftRadius: isTop ? kfT : INSTANT_TRANSITION,\n        borderTopRightRadius: isTop ? kfT : INSTANT_TRANSITION,\n        borderBottomLeftRadius: isTop ? INSTANT_TRANSITION : kfT,\n        borderBottomRightRadius: isTop ? INSTANT_TRANSITION : kfT,\n      }}\n      className={cn(\n        \"relative z-10 flex w-full items-center justify-between gap-2 rounded-xl border border-border bg-background px-3 py-2 text-sm text-foreground outline-none transition-colors\",\n        \"hover:border-(--color-border-strong) focus-visible:ring-2 focus-visible:ring-foreground/20\",\n        \"disabled:pointer-events-none disabled:opacity-50\",\n        className,\n      )}\n    >\n      {children}\n      <motion.span\n        aria-hidden\n        animate={{ rotate: ctx.open ? 180 : 0 }}\n        transition={ctx.reduce ? { duration: 0 } : CHEVRON_TRANSITION}\n        className=\"text-muted-foreground\"\n      >\n        <ChevronDown className=\"h-4 w-4\" />\n      </motion.span>\n    </motion.button>\n  );\n}\n\nexport interface SelectValueProps {\n  placeholder?: string;\n  className?: string;\n}\n\nexport function SelectValue({ placeholder, className }: SelectValueProps) {\n  const ctx = useSelectContext(\"SelectValue\");\n  const label = ctx.labelFor(ctx.value);\n  return (\n    <span\n      className={cn(label ? \"text-foreground\" : \"text-muted-foreground\", className)}\n    >\n      {label ?? placeholder ?? \"Select\"}\n    </span>\n  );\n}\n\nexport interface SelectContentProps {\n  className?: string;\n  children: ReactNode;\n}\n\nexport function SelectContent({ className, children }: SelectContentProps) {\n  const ctx = useSelectContext(\"SelectContent\");\n  const innerRef = useRef<HTMLDivElement>(null);\n  const [height, setHeight] = useState(0);\n  const open = ctx.open;\n  const { setPlacement } = ctx;\n\n  useLayoutEffect(() => {\n    const node = innerRef.current;\n    if (!node) return;\n    const measure = () => setHeight(node.offsetHeight);\n    measure();\n    const observer = new ResizeObserver(measure);\n    observer.observe(node);\n    return () => observer.disconnect();\n  });\n\n  // On open, flip upward when there isn't room below and there's more above.\n  useLayoutEffect(() => {\n    if (!open) return;\n    const trigger = document.getElementById(ctx.triggerId);\n    const node = innerRef.current;\n    if (!trigger || !node) return;\n    const rect = trigger.getBoundingClientRect();\n    const h = node.offsetHeight;\n    const below = window.innerHeight - rect.bottom;\n    const above = rect.top;\n    setPlacement(below < h + 16 && above > below ? \"top\" : \"bottom\");\n  }, [open, ctx.triggerId, setPlacement]);\n\n  // Specify EVERY corner + both margins each render. The near edge (facing the\n  // trigger) animates flat->round and the gap opens on that side; the far edge\n  // stays rounded and its margin pinned to 0. Setting all of them avoids a\n  // stranded square corner when the placement flips between opens.\n  const isTop = ctx.placement === \"top\";\n  const nearGap = open ? 8 : 0;\n  const nearRadius = open ? 12 : 0;\n\n  const gapT: Transition = open\n    ? { type: \"spring\", duration: 0.6, bounce: 0.5, delay: 0.12 }\n    : { type: \"spring\", duration: 0.3, bounce: 0.1 };\n  const radiusT: Transition = open\n    ? { duration: 0.3, ease: EASE_OUT, delay: 0.14 }\n    : { duration: 0.16, ease: EASE_OUT };\n\n  // Items stay mounted (open just animates the panel) so each item's label\n  // registration persists — otherwise the trigger would fall back to the\n  // placeholder the moment the panel closes.\n  return (\n    <motion.div\n      id={ctx.listId}\n      role=\"listbox\"\n      aria-labelledby={ctx.triggerId}\n      aria-hidden={!open}\n      inert={!open}\n      initial={false}\n      animate={\n        ctx.reduce\n          ? { opacity: open ? 1 : 0, height: open ? height : 0 }\n          : {\n              opacity: open ? 1 : 0,\n              height: open ? height : 0,\n              // gap opens on the side facing the trigger\n              marginTop: isTop ? 0 : nearGap,\n              marginBottom: isTop ? nearGap : 0,\n              // near corners go flat->round; far corners stay rounded\n              borderTopLeftRadius: isTop ? 12 : nearRadius,\n              borderTopRightRadius: isTop ? 12 : nearRadius,\n              borderBottomLeftRadius: isTop ? nearRadius : 12,\n              borderBottomRightRadius: isTop ? nearRadius : 12,\n            }\n      }\n      transition={\n        ctx.reduce\n          ? { duration: 0.12 }\n          : {\n              opacity: open\n                ? { duration: 0.18 }\n                : { duration: 0.16, delay: 0.12 },\n              height: open\n                ? { type: \"spring\", duration: 0.42, bounce: 0.14 }\n                : { duration: 0.26, ease: EASE_OUT, delay: 0.14 },\n              marginTop: isTop ? INSTANT_TRANSITION : gapT,\n              marginBottom: isTop ? gapT : INSTANT_TRANSITION,\n              borderTopLeftRadius: isTop ? INSTANT_TRANSITION : radiusT,\n              borderTopRightRadius: isTop ? INSTANT_TRANSITION : radiusT,\n              borderBottomLeftRadius: isTop ? radiusT : INSTANT_TRANSITION,\n              borderBottomRightRadius: isTop ? radiusT : INSTANT_TRANSITION,\n            }\n      }\n      style={{\n        transformOrigin: isTop ? \"bottom\" : \"top\",\n        overflow: \"hidden\",\n        pointerEvents: open ? \"auto\" : \"none\",\n      }}\n      // flush against the trigger, then separates into its own rounded pill;\n      // sits above or below depending on available space\n      className={cn(\n        \"absolute left-0 right-0 z-20 rounded-xl border border-border bg-background shadow-lg\",\n        isTop ? \"bottom-full\" : \"top-full\",\n        className,\n      )}\n    >\n      <motion.div\n        ref={innerRef}\n        variants={ctx.reduce ? undefined : LIST_VARIANTS}\n        initial={false}\n        animate={open ? \"show\" : \"hidden\"}\n        className=\"p-1\"\n      >\n        {children}\n      </motion.div>\n    </motion.div>\n  );\n}\n\nexport interface SelectItemProps {\n  value: string;\n  disabled?: boolean;\n  className?: string;\n  children: ReactNode;\n}\n\nexport function SelectItem({\n  value,\n  disabled = false,\n  className,\n  children,\n}: SelectItemProps) {\n  const ctx = useSelectContext(\"SelectItem\");\n  const selected = ctx.value === value;\n  const label = typeof children === \"string\" ? children : value;\n\n  useLayoutEffect(() => {\n    ctx.register(value, label);\n    return () => ctx.unregister(value);\n  }, [ctx.register, ctx.unregister, value, label]);\n\n  return (\n    <motion.li variants={ctx.reduce ? undefined : ITEM_VARIANTS}>\n      <button\n        type=\"button\"\n        role=\"option\"\n        aria-selected={selected}\n        disabled={disabled}\n        onClick={() => ctx.select(value)}\n        className={cn(\n          \"flex w-full items-center justify-between gap-2 rounded-lg px-2.5 py-1.5 text-left text-sm outline-none transition-colors\",\n          selected\n            ? \"bg-muted text-foreground\"\n            : \"text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:bg-muted\",\n          \"disabled:pointer-events-none disabled:opacity-50\",\n          className,\n        )}\n      >\n        {children}\n        {selected ? <Check className=\"h-3.5 w-3.5 shrink-0\" /> : null}\n      </button>\n    </motion.li>\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":"components/motion/shared-layout-bg.tsx","type":"registry:component","target":"@components/motion/shared-layout-bg.tsx","content":"\"use client\";\n\nimport {\n  AnimatePresence,\n  type HTMLMotionProps,\n  motion,\n  useReducedMotion,\n  type Variants,\n} from \"motion/react\";\nimport {\n  Children,\n  cloneElement,\n  forwardRef,\n  type HTMLAttributes,\n  isValidElement,\n  type MouseEvent,\n  type ReactElement,\n  type ReactNode,\n  type Ref,\n  useId,\n  useState,\n} from \"react\";\nimport { SPRING_LAYOUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface SharedLayoutBgProps\n  extends Omit<HTMLAttributes<HTMLElement>, \"children\"> {\n  children: ReactNode;\n  /** Semantic container used for the children. */\n  as?: \"div\" | \"ul\";\n  /** Tailwind class applied to the moving pill. Defaults to a subtle foreground tint. */\n  pillClassName?: string;\n  /** Horizontal inset of the pill relative to each row (px). Default 20. */\n  inset?: number;\n  /** Optional positioning override for the pill wrapper inside each item. */\n  pillContainerClassName?: string;\n}\n\nconst variants: Variants = {\n  initial: { opacity: 0, filter: \"blur(6px)\" },\n  animate: { opacity: 1, filter: \"blur(0px)\" },\n  exit: (isActive: boolean) =>\n    !isActive ? { opacity: 0, filter: \"blur(6px)\" } : {},\n};\n\nconst reducedVariants: Variants = {\n  initial: { opacity: 0 },\n  animate: { opacity: 1 },\n  exit: (isActive: boolean) => (!isActive ? { opacity: 0 } : {}),\n};\n\nexport const SharedLayoutBg = forwardRef<HTMLElement, SharedLayoutBgProps>(\n  function SharedLayoutBg(\n    {\n      children,\n      as = \"div\",\n      className,\n      onMouseLeave,\n      pillClassName,\n      pillContainerClassName,\n      inset = 20,\n      ...props\n    },\n    forwardedRef,\n  ) {\n  const [activeId, setActiveId] = useState<string | null>(null);\n  const uid = useId();\n  const reduce = useReducedMotion();\n\n    const renderedChildren = Children.toArray(children)\n      .filter(isValidElement)\n      .map((child, index) => {\n        const el = child as ReactElement<{\n          className?: string;\n          onMouseEnter?: () => void;\n          children?: ReactNode;\n        }>;\n        const childKey = el.key ? String(el.key) : `item-${index}`;\n        return cloneElement(\n          el,\n          {\n            key: childKey,\n            className: cn(\"relative\", el.props.className),\n            onMouseEnter: () => {\n              el.props.onMouseEnter?.();\n              setActiveId(childKey);\n            },\n          },\n          <>\n            <AnimatePresence custom={activeId !== null}>\n              {activeId !== null ? (\n                <motion.div\n                  variants={reduce ? reducedVariants : variants}\n                  initial=\"initial\"\n                  animate=\"animate\"\n                  exit=\"exit\"\n                  custom={activeId !== null}\n                  className={cn(\n                    \"pointer-events-none absolute inset-y-0\",\n                    pillContainerClassName,\n                  )}\n                  style={{ left: -inset, right: -inset }}\n                >\n                  {activeId === childKey ? (\n                    <motion.div\n                      layoutId={`shared-bg-${uid}`}\n                      transition={reduce ? { duration: 0 } : SPRING_LAYOUT}\n                      className={cn(\n                        \"pointer-events-none h-full w-full rounded-2xl bg-primary/[0.06]\",\n                        pillClassName,\n                      )}\n                    />\n                  ) : null}\n                </motion.div>\n              ) : null}\n            </AnimatePresence>\n            <div className=\"relative z-10\">{el.props.children}</div>\n          </>,\n        );\n      });\n\n    const handleMouseLeave = (event: MouseEvent<HTMLElement>) => {\n      setActiveId(null);\n      onMouseLeave?.(event);\n    };\n\n    // layoutRoot scopes the pill's layout projection to this list, so fixed or\n    // scrolled ancestors can't smear scroll offsets into its movement.\n    return as === \"ul\" ? (\n      <motion.ul\n        {...(props as HTMLMotionProps<\"ul\">)}\n        ref={forwardedRef as Ref<HTMLUListElement>}\n        layoutRoot\n        onMouseLeave={handleMouseLeave}\n        className={cn(\"flex w-full flex-col\", className)}\n      >\n        {renderedChildren}\n      </motion.ul>\n    ) : (\n      <motion.div\n        {...(props as HTMLMotionProps<\"div\">)}\n        ref={forwardedRef as Ref<HTMLDivElement>}\n        layoutRoot\n        onMouseLeave={handleMouseLeave}\n        className={cn(\"flex w-full flex-col\", className)}\n      >\n        {renderedChildren}\n      </motion.div>\n    );\n  },\n);\n"},{"path":"components/motion/popover-position.ts","type":"registry:component","target":"@components/motion/popover-position.ts","content":"\"use client\";\n\nimport {\n  type MutableRefObject,\n  useCallback,\n  useLayoutEffect,\n  useState,\n} from \"react\";\n\nexport type PortalLayout = {\n  trigger: {\n    left: number;\n    top: number;\n    width: number;\n    height: number;\n  };\n  content: {\n    width: number;\n    height: number;\n  };\n};\n\nfunction sameLayout(a: PortalLayout | null, b: PortalLayout) {\n  return (\n    a?.trigger.left === b.trigger.left &&\n    a.trigger.top === b.trigger.top &&\n    a.trigger.width === b.trigger.width &&\n    a.trigger.height === b.trigger.height &&\n    a.content.width === b.content.width &&\n    a.content.height === b.content.height\n  );\n}\n\n/** Measures a trigger and portalled panel in viewport coordinates. */\nexport function usePopoverPortalPosition<\n  TriggerElement extends HTMLElement,\n  ContentElement extends HTMLElement,\n>(\n  triggerRef: MutableRefObject<TriggerElement | null>,\n  contentRef: MutableRefObject<ContentElement | null>,\n  active: boolean,\n) {\n  const [layout, setLayout] = useState<PortalLayout | null>(null);\n\n  const update = useCallback(() => {\n    const trigger = triggerRef.current;\n    const content = contentRef.current;\n    if (!trigger || !content) return;\n\n    const rect = trigger.getBoundingClientRect();\n    const next: PortalLayout = {\n      trigger: {\n        left: rect.left,\n        top: rect.top,\n        width: rect.width,\n        height: rect.height,\n      },\n      content: {\n        width: content.offsetWidth,\n        height: content.offsetHeight,\n      },\n    };\n    setLayout((current) => (sameLayout(current, next) ? current : next));\n  }, [contentRef, triggerRef]);\n\n  useLayoutEffect(() => {\n    update();\n    if (!active) return;\n\n    const trigger = triggerRef.current;\n    const content = contentRef.current;\n    const observer = new ResizeObserver(update);\n    if (trigger) observer.observe(trigger);\n    if (content) observer.observe(content);\n\n    window.addEventListener(\"scroll\", update, true);\n    window.addEventListener(\"resize\", update);\n    return () => {\n      observer.disconnect();\n      window.removeEventListener(\"scroll\", update, true);\n      window.removeEventListener(\"resize\", update);\n    };\n  }, [active, contentRef, triggerRef, update]);\n\n  return layout;\n}\n"},{"path":"components/motion/action-swap.tsx","type":"registry:component","target":"@components/motion/action-swap.tsx","content":"\"use client\";\n\nimport { AnimatePresence, motion, useReducedMotion, type HTMLMotionProps, type Variants } from \"motion/react\";\nimport { useLayoutEffect, useRef, useState, type ReactNode } from \"react\";\nimport { EASE_OUT, EASE_OUT_CSS, SPRING_PRESS, SPRING_SWAP } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport type ActionSwapItem = {\n  id: string;\n  label: ReactNode;\n  icon?: ReactNode;\n  ariaLabel?: string;\n};\n\nexport type ActionSwapButtonVariant = \"primary\" | \"secondary\" | \"outline\" | \"ghost\";\nexport type ActionSwapButtonSize = \"sm\" | \"md\" | \"lg\" | \"icon\";\nexport type ActionSwapAnimation = \"blur\" | \"roll\" | \"cascade\";\n\n/** Animations with a single-element variant set (cascade animates per letter). */\ntype CoreAnimation = \"blur\" | \"roll\";\n\nexport interface ActionSwapButtonProps extends Omit<\n  HTMLMotionProps<\"button\">,\n  \"children\" | \"onChange\"\n> {\n  items: ActionSwapItem[];\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string, item: ActionSwapItem) => void;\n  variant?: ActionSwapButtonVariant;\n  size?: ActionSwapButtonSize;\n  animation?: ActionSwapAnimation;\n  iconOnly?: boolean;\n  cycle?: boolean;\n}\n\nexport interface ActionSwapTextProps {\n  value: string;\n  children: ReactNode;\n  animation?: ActionSwapAnimation;\n  className?: string;\n}\n\nexport interface ActionSwapIconProps {\n  value: string;\n  children: ReactNode;\n  animation?: ActionSwapAnimation;\n  className?: string;\n}\n\nconst BLUR_TRANSITION = { duration: 0.2, ease: \"easeInOut\" } as const;\nconst ROLL_TRANSITION = SPRING_SWAP;\nconst ROLL_EXIT_TRANSITION = { duration: 0.14, ease: EASE_OUT } as const;\nconst SWAP_BLUR = \"blur(8px)\";\nconst ROLL_BLUR = \"blur(3px)\";\n\n// Cascade rolls the label one letter at a time, left to right. The leaving\n// and landing strings overlap as independent layers (no shared cells), so\n// proportional glyph widths never jitter. Exits cascade at half the enter\n// stagger so the tail of the old label lingers briefly.\nconst CASCADE_STAGGER = 0.025;\n\nconst CASCADE_LETTER_VARIANTS: Variants = {\n  initial: { opacity: 0, y: \"105%\", filter: ROLL_BLUR },\n  animate: (delay: number = 0) => ({\n    opacity: 1,\n    y: \"0%\",\n    filter: \"blur(0px)\",\n    transition: { ...SPRING_SWAP, delay },\n  }),\n  exit: (delay: number = 0) => ({\n    opacity: 0,\n    y: \"-105%\",\n    filter: ROLL_BLUR,\n    transition: { duration: 0.16, ease: EASE_OUT, delay: delay * 0.5 },\n  }),\n};\n\nconst TEXT_VARIANTS: Record<CoreAnimation, Variants> = {\n  blur: {\n    initial: { opacity: 0, scale: 0.94, filter: SWAP_BLUR },\n    animate: {\n      opacity: 1,\n      scale: 1,\n      filter: \"blur(0px)\",\n      transition: BLUR_TRANSITION,\n    },\n    exit: {\n      opacity: 0,\n      scale: 0.94,\n      filter: SWAP_BLUR,\n      transition: BLUR_TRANSITION,\n    },\n  },\n  roll: {\n    initial: { opacity: 0, y: \"90%\", filter: ROLL_BLUR },\n    animate: {\n      opacity: 1,\n      y: \"0%\",\n      filter: \"blur(0px)\",\n      transition: ROLL_TRANSITION,\n    },\n    exit: {\n      opacity: 0,\n      y: \"-90%\",\n      filter: ROLL_BLUR,\n      transition: ROLL_EXIT_TRANSITION,\n    },\n  },\n};\n\nconst ICON_VARIANTS: Record<CoreAnimation, Variants> = {\n  blur: {\n    initial: { opacity: 0, scale: 0.25, filter: SWAP_BLUR },\n    animate: {\n      opacity: 1,\n      scale: 1,\n      filter: \"blur(0px)\",\n      transition: BLUR_TRANSITION,\n    },\n    exit: {\n      opacity: 0,\n      scale: 0.25,\n      filter: SWAP_BLUR,\n      transition: BLUR_TRANSITION,\n    },\n  },\n  roll: {\n    initial: { opacity: 0, y: 12, filter: ROLL_BLUR },\n    animate: {\n      opacity: 1,\n      y: 0,\n      filter: \"blur(0px)\",\n      transition: ROLL_TRANSITION,\n    },\n    exit: {\n      opacity: 0,\n      y: -12,\n      filter: ROLL_BLUR,\n      transition: ROLL_EXIT_TRANSITION,\n    },\n  },\n};\n\nconst VARIANT_CLASS: Record<ActionSwapButtonVariant, string> = {\n  primary: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n  secondary: \"border border-border bg-card text-foreground hover:border-border\",\n  outline: \"border border-border bg-transparent text-foreground hover:bg-primary/5\",\n  ghost: \"text-muted-foreground hover:bg-primary/5 hover:text-foreground\",\n};\n\nconst SIZE_CLASS: Record<ActionSwapButtonSize, string> = {\n  sm: \"h-8 gap-1.5 rounded-full px-3 text-xs\",\n  md: \"h-10 gap-2 rounded-full px-4 text-sm\",\n  lg: \"h-12 gap-2.5 rounded-full px-5 text-base\",\n  icon: \"h-10 w-10 rounded-full\",\n};\n\nexport function ActionSwapText({\n  value,\n  children,\n  animation = \"blur\",\n  className,\n}: ActionSwapTextProps) {\n  const reduce = useReducedMotion();\n  const measureRef = useRef<HTMLSpanElement>(null);\n  const [width, setWidth] = useState<number>();\n\n  useLayoutEffect(() => {\n    const nextWidth = measureRef.current?.offsetWidth;\n    if (!nextWidth) return;\n    setWidth((currentWidth) => (currentWidth === nextWidth ? currentWidth : nextWidth));\n  });\n\n  // Cascade needs a plain string to split into letters; non-string content\n  // and reduced motion fall back to the closest single-element animation.\n  const label = typeof children === \"string\" ? children : null;\n  const cascade = animation === \"cascade\" && label !== null && !reduce;\n  const coreAnimation: CoreAnimation =\n    animation === \"cascade\" ? \"roll\" : animation;\n\n  return (\n    <span\n      className={cn(\"relative inline-block overflow-hidden whitespace-nowrap align-bottom\", className)}\n      style={{\n        width,\n        transition: reduce ? undefined : `width 220ms ${EASE_OUT_CSS}`,\n      }}\n    >\n      <span\n        ref={measureRef}\n        aria-hidden\n        className=\"invisible inline-block whitespace-nowrap\"\n      >\n        {children}\n      </span>\n      {cascade ? (\n        <>\n          {/* Letters are decorative fragments; readers get the whole label. */}\n          <span className=\"sr-only\">{label}</span>\n          <AnimatePresence initial={false}>\n            <motion.span\n              key={`cascade-${value}`}\n              aria-hidden\n              initial=\"initial\"\n              animate=\"animate\"\n              exit=\"exit\"\n              className=\"absolute left-0 top-0 inline-block whitespace-pre\"\n            >\n              {label.split(\"\").map((char, i) => (\n                <motion.span\n                  // biome-ignore lint/suspicious/noArrayIndexKey: position is the slot identity — the letter at a position is exactly what rolls.\n                  key={i}\n                  custom={i * CASCADE_STAGGER}\n                  variants={CASCADE_LETTER_VARIANTS}\n                  className=\"inline-block whitespace-pre will-change-[opacity,filter,transform]\"\n                >\n                  {char}\n                </motion.span>\n              ))}\n            </motion.span>\n          </AnimatePresence>\n        </>\n      ) : (\n        <AnimatePresence initial={false}>\n          <motion.span\n            key={`${animation}-${value}`}\n            variants={TEXT_VARIANTS[coreAnimation]}\n            initial={reduce ? false : \"initial\"}\n            animate={reduce ? { opacity: 1, filter: \"blur(0px)\", scale: 1, y: 0 } : \"animate\"}\n            exit={reduce ? undefined : \"exit\"}\n            className=\"absolute left-0 top-0 inline-block will-change-[opacity,filter,transform]\"\n          >\n            {children}\n          </motion.span>\n        </AnimatePresence>\n      )}\n    </span>\n  );\n}\n\nexport function ActionSwapIcon({\n  value,\n  children,\n  animation = \"blur\",\n  className,\n}: ActionSwapIconProps) {\n  const reduce = useReducedMotion();\n  // Icons are single elements — cascade maps to its closest motion, roll.\n  const coreAnimation: CoreAnimation =\n    animation === \"cascade\" ? \"roll\" : animation;\n\n  return (\n    <span className={cn(\"relative inline-grid shrink-0 place-items-center overflow-hidden\", className)}>\n      <AnimatePresence mode=\"popLayout\" initial={false}>\n        <motion.span\n          key={`${animation}-${value}`}\n          aria-hidden\n          variants={ICON_VARIANTS[coreAnimation]}\n          initial={reduce ? false : \"initial\"}\n          animate={reduce ? { opacity: 1, filter: \"blur(0px)\", scale: 1, y: 0 } : \"animate\"}\n          exit={reduce ? undefined : \"exit\"}\n          className=\"col-start-1 row-start-1 inline-flex items-center justify-center will-change-[opacity,filter,transform]\"\n        >\n          {children}\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  );\n}\n\nexport function ActionSwapButton({\n  items,\n  value,\n  defaultValue,\n  onValueChange,\n  variant = \"secondary\",\n  size = \"md\",\n  animation = \"blur\",\n  iconOnly = size === \"icon\",\n  cycle = true,\n  className,\n  disabled,\n  onClick,\n  ...rest\n}: ActionSwapButtonProps) {\n  const reduce = useReducedMotion();\n  const [internalValue, setInternalValue] = useState(defaultValue ?? items[0]?.id);\n  const currentValue = value ?? internalValue;\n  const activeIndex = Math.max(0, items.findIndex((item) => item.id === currentValue));\n  const activeItem = items[activeIndex] ?? items[0];\n  const hasIcon = items.some((item) => item.icon);\n  const nextItem = cycle && items.length > 0 ? items[(activeIndex + 1) % items.length] : undefined;\n\n  if (!activeItem) return null;\n\n  const accessibleLabel = activeItem.ariaLabel ?? (iconOnly && typeof activeItem.label === \"string\" ? activeItem.label : undefined);\n\n  return (\n    <motion.button\n      type=\"button\"\n      disabled={disabled}\n      whileTap={reduce || disabled ? undefined : { scale: 0.97 }}\n      transition={SPRING_PRESS}\n      className={cn(\n        \"inline-flex items-center justify-center overflow-hidden font-medium transition-colors\",\n        \"disabled:pointer-events-none disabled:opacity-50\",\n        VARIANT_CLASS[variant],\n        SIZE_CLASS[size],\n        className,\n      )}\n      aria-label={accessibleLabel}\n      onClick={(event) => {\n        onClick?.(event);\n        if (event.defaultPrevented || disabled || !cycle || !nextItem) return;\n        if (value === undefined) setInternalValue(nextItem.id);\n        onValueChange?.(nextItem.id, nextItem);\n      }}\n      {...rest}\n    >\n      {hasIcon ? (\n        <ActionSwapIcon value={activeItem.id} animation={animation} className=\"h-4 w-4\">\n          {activeItem.icon ?? null}\n        </ActionSwapIcon>\n      ) : null}\n      {!iconOnly ? (\n        <ActionSwapText value={activeItem.id} animation={animation}>\n          {activeItem.label}\n        </ActionSwapText>\n      ) : null}\n    </motion.button>\n  );\n}\n"},{"path":"components/motion/button/base.tsx","type":"registry:component","target":"@components/motion/button/base.tsx","content":"\"use client\";\n\nimport {\n  AnimatePresence,\n  motion,\n  useReducedMotion,\n  type HTMLMotionProps,\n} from \"motion/react\";\nimport {\n  forwardRef,\n  type PointerEvent,\n  type ReactNode,\n  useCallback,\n  useRef,\n  useState,\n} from \"react\";\nimport { EASE_OUT, SPRING_PRESS } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\nimport { useHoverCapable } from \"@/lib/hooks/use-hover-capable\";\n\nexport type ButtonVariant = \"primary\" | \"secondary\" | \"ghost\" | \"outline\";\nexport type ButtonSize = \"sm\" | \"md\" | \"lg\" | \"icon\";\n\nexport interface ButtonProps extends Omit<\n  HTMLMotionProps<\"button\">,\n  \"children\"\n> {\n  variant?: ButtonVariant;\n  size?: ButtonSize;\n  pressScale?: number;\n  /** Spawn a Material-style ripple from the press point. Off by default. */\n  ripple?: boolean;\n  children?: ReactNode;\n}\n\ntype Ripple = { id: number; x: number; y: number; size: number };\n\nconst VARIANT_CLASS: Record<ButtonVariant, string> = {\n  primary: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n  secondary: \"border border-border bg-card text-foreground hover:border-border\",\n  ghost: \"text-muted-foreground hover:text-foreground hover:bg-primary/5\",\n  outline:\n    \"border border-border bg-transparent text-foreground hover:bg-primary/5\",\n};\n\nconst SIZE_CLASS: Record<ButtonSize, string> = {\n  sm: \"h-8 px-3 text-xs gap-1.5 rounded-full\",\n  md: \"h-10 px-5 text-sm gap-2 rounded-full\",\n  lg: \"h-12 px-6 text-base gap-2 rounded-full\",\n  icon: \"h-8 w-8 rounded-lg\",\n};\n\nexport const Button = forwardRef<HTMLButtonElement, ButtonProps>(\n  function Button(\n    {\n      variant = \"primary\",\n      size = \"md\",\n      pressScale = 0.93,\n      ripple = false,\n      className,\n      children,\n      onPointerDown,\n      ...rest\n    },\n    ref,\n  ) {\n    const reduce = useReducedMotion();\n    const canHover = useHoverCapable();\n    const [ripples, setRipples] = useState<Ripple[]>([]);\n    const nextId = useRef(0);\n\n    const handlePointerDown = useCallback(\n      (event: PointerEvent<HTMLButtonElement>) => {\n        if (ripple && !reduce) {\n          const rect = event.currentTarget.getBoundingClientRect();\n          const size = Math.max(rect.width, rect.height) * 2;\n          const id = nextId.current++;\n          setRipples((prev) => [\n            ...prev,\n            {\n              id,\n              x: event.clientX - rect.left,\n              y: event.clientY - rect.top,\n              size,\n            },\n          ]);\n        }\n        onPointerDown?.(event);\n      },\n      [ripple, reduce, onPointerDown],\n    );\n\n    return (\n      <motion.button\n        ref={ref}\n        type=\"button\"\n        whileTap={reduce ? undefined : { scale: pressScale }}\n        whileHover={reduce || !canHover ? undefined : { scale: 1.02 }}\n        transition={SPRING_PRESS}\n        onPointerDown={handlePointerDown}\n        className={cn(\n          \"inline-flex items-center justify-center font-medium select-none\",\n          \"transition-colors\",\n          \"disabled:pointer-events-none disabled:opacity-50\",\n          ripple && \"relative overflow-hidden\",\n          VARIANT_CLASS[variant],\n          SIZE_CLASS[size],\n          className,\n        )}\n        {...rest}\n      >\n        {ripple && !reduce ? (\n          <span className=\"pointer-events-none absolute inset-0 overflow-hidden rounded-[inherit]\">\n            <AnimatePresence>\n              {ripples.map((r) => (\n                <motion.span\n                  key={r.id}\n                  className=\"absolute rounded-full bg-current\"\n                  style={{\n                    left: r.x,\n                    top: r.y,\n                    width: r.size,\n                    height: r.size,\n                    x: \"-50%\",\n                    y: \"-50%\",\n                  }}\n                  initial={{ scale: 0.05, opacity: 0.3 }}\n                  animate={{ scale: 1, opacity: 0 }}\n                  exit={{ opacity: 0 }}\n                  transition={{ duration: 1.6, ease: EASE_OUT }}\n                  onAnimationComplete={() =>\n                    setRipples((prev) => prev.filter((x) => x.id !== r.id))\n                  }\n                />\n              ))}\n            </AnimatePresence>\n          </span>\n        ) : null}\n        {children}\n      </motion.button>\n    );\n  },\n);\n"},{"path":"components/motion/button/magnetic.tsx","type":"registry:component","target":"@components/motion/button/magnetic.tsx","content":"\"use client\";\n\nimport { forwardRef } from \"react\";\nimport { Magnetic } from \"../magnetic\";\nimport { Button, type ButtonProps } from \"./base\";\n\nexport interface MagneticButtonProps extends ButtonProps {\n  /** Magnetic pull strength. Default 0.25. */\n  strength?: number;\n  /** Class applied to the magnetic wrapper. */\n  magneticClassName?: string;\n}\n\nexport const MagneticButton = forwardRef<HTMLButtonElement, MagneticButtonProps>(function MagneticButton(\n  { strength = 0.25, magneticClassName, children, ...rest },\n  ref,\n) {\n  return (\n    <Magnetic strength={strength} className={magneticClassName}>\n      <Button ref={ref} {...rest}>\n        {children}\n      </Button>\n    </Magnetic>\n  );\n});\n"},{"path":"components/motion/button/stateful.tsx","type":"registry:component","target":"@components/motion/button/stateful.tsx","content":"\"use client\";\n\nimport { Check, Loader2, X } from \"lucide-react\";\nimport {\n  AnimatePresence,\n  motion,\n  useReducedMotion,\n  type Variants,\n} from \"motion/react\";\nimport {\n  forwardRef,\n  type ReactNode,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { EASE_OUT, SPRING_SWAP } from \"@/lib/ease\";\nimport { Button, type ButtonProps } from \"./base\";\n\nexport type ButtonState = \"idle\" | \"loading\" | \"success\" | \"error\";\n\nexport interface StatefulButtonProps extends Omit<ButtonProps, \"children\"> {\n  state?: ButtonState;\n  children: ReactNode;\n  loadingText?: ReactNode;\n  successText?: ReactNode;\n  errorText?: ReactNode;\n  icon?: ReactNode;\n}\n\nconst CASCADE_STAGGER = 0.025;\nconst ROLL_BLUR = \"blur(6px)\";\n\nconst CASCADE_LETTER_VARIANTS: Variants = {\n  initial: { opacity: 0, y: \"105%\", filter: ROLL_BLUR },\n  animate: (delay: number = 0) => ({\n    opacity: 1,\n    y: \"0%\",\n    filter: \"blur(0px)\",\n    transition: { ...SPRING_SWAP, delay },\n  }),\n  exit: (delay: number = 0) => ({\n    opacity: 0,\n    y: \"-105%\",\n    filter: ROLL_BLUR,\n    transition: { duration: 0.16, ease: EASE_OUT, delay: delay * 0.5 },\n  }),\n};\n\nconst ICON_VARIANTS: Variants = {\n  // Width collapses too, so the icon adds/removes its own space smoothly\n  // instead of popping the row width in a single frame.\n  initial: { opacity: 0, width: 0, scale: 0.7, filter: ROLL_BLUR },\n  animate: {\n    opacity: 1,\n    width: \"1.5rem\",\n    scale: 1,\n    filter: \"blur(0px)\",\n    transition: SPRING_SWAP,\n  },\n  exit: {\n    opacity: 0,\n    width: 0,\n    scale: 0.7,\n    filter: ROLL_BLUR,\n    transition: { duration: 0.16, ease: EASE_OUT },\n  },\n};\n\nfunction IconSlot({ keyId, children }: { keyId: string; children: ReactNode }) {\n  const reduce = useReducedMotion();\n  return (\n    <motion.span\n      key={keyId}\n      variants={ICON_VARIANTS}\n      initial={reduce ? { opacity: 0 } : \"initial\"}\n      animate={reduce ? { opacity: 1 } : \"animate\"}\n      exit={reduce ? { opacity: 0 } : \"exit\"}\n      transition={reduce ? { duration: 0.15 } : undefined}\n      className=\"inline-grid shrink-0 place-items-center overflow-hidden\"\n    >\n      {children}\n    </motion.span>\n  );\n}\n\nfunction TextSlot({\n  value,\n  children,\n}: {\n  value: string;\n  children: ReactNode;\n}) {\n  const reduce = useReducedMotion();\n  const measureRef = useRef<HTMLSpanElement>(null);\n  const [width, setWidth] = useState<number>();\n  const label = typeof children === \"string\" ? children : null;\n  const cascade = label !== null && !reduce;\n\n  // Measure strings with the same per-letter layout as the cascade. Measuring\n  // the whole string preserves kerning, which can make it narrower than the\n  // inline-block letters and clip the final glyph during the width animation.\n  useLayoutEffect(() => {\n    const nextWidth = measureRef.current?.offsetWidth;\n    if (!nextWidth) return;\n    setWidth((current) => (current === nextWidth ? current : nextWidth));\n  });\n\n  return (\n    <motion.span\n      initial={false}\n      animate={{ width }}\n      transition={reduce ? { duration: 0 } : SPRING_SWAP}\n      className=\"relative inline-block overflow-hidden whitespace-nowrap align-bottom\"\n    >\n      <span\n        ref={measureRef}\n        aria-hidden\n        className=\"invisible inline-block whitespace-nowrap\"\n      >\n        {cascade\n          ? label.split(\"\").map((char, index) => (\n              <span\n                // biome-ignore lint/suspicious/noArrayIndexKey: position is the slot identity.\n                key={index}\n                className=\"inline-block whitespace-pre\"\n              >\n                {char}\n              </span>\n            ))\n          : children}\n      </span>\n\n      {cascade ? (\n        <>\n          <span className=\"sr-only\">{label}</span>\n          <AnimatePresence initial={false}>\n            <motion.span\n              key={`cascade-${value}`}\n              aria-hidden\n              initial=\"initial\"\n              animate=\"animate\"\n              exit=\"exit\"\n              className=\"absolute left-0 top-0 inline-block whitespace-pre\"\n            >\n              {label.split(\"\").map((char, index) => (\n                <motion.span\n                  // biome-ignore lint/suspicious/noArrayIndexKey: position is the slot identity.\n                  key={index}\n                  custom={index * CASCADE_STAGGER}\n                  variants={CASCADE_LETTER_VARIANTS}\n                  className=\"inline-block whitespace-pre will-change-[opacity,filter,transform]\"\n                >\n                  {char}\n                </motion.span>\n              ))}\n            </motion.span>\n          </AnimatePresence>\n        </>\n      ) : (\n        <AnimatePresence initial={false}>\n          <motion.span\n            key={`text-${value}`}\n            initial={reduce ? { opacity: 0 } : { opacity: 0, y: 14, filter: ROLL_BLUR }}\n            animate={reduce ? { opacity: 1 } : { opacity: 1, y: 0, filter: \"blur(0px)\" }}\n            exit={reduce ? { opacity: 0 } : { opacity: 0, y: -14, filter: ROLL_BLUR }}\n            transition={reduce ? { duration: 0.15 } : SPRING_SWAP}\n            className=\"absolute left-0 top-0 inline-block will-change-[opacity,filter,transform]\"\n          >\n            {children}\n          </motion.span>\n        </AnimatePresence>\n      )}\n    </motion.span>\n  );\n}\n\nexport const StatefulButton = forwardRef<HTMLButtonElement, StatefulButtonProps>(function StatefulButton(\n  {\n    state = \"idle\",\n    children,\n    loadingText = \"Loading\",\n    successText = \"Done\",\n    errorText = \"Try again\",\n    icon,\n    disabled,\n    ...rest\n  },\n  ref,\n) {\n  const isBusy = state === \"loading\";\n  const stateText =\n    state === \"loading\"\n      ? loadingText\n      : state === \"success\"\n        ? successText\n        : state === \"error\"\n        ? errorText\n        : children;\n  const textKey =\n    typeof stateText === \"string\" ? `${state}-${stateText}` : state;\n\n  return (\n    <Button ref={ref} disabled={disabled || isBusy} aria-busy={isBusy} whileHover={undefined} {...rest}>\n      <span\n        aria-live=\"polite\"\n        className=\"relative inline-flex items-center justify-center overflow-hidden\"\n      >\n        <AnimatePresence initial={false}>\n          {state === \"loading\" ? (\n            <IconSlot keyId=\"loading-icon\">\n              <Loader2 className=\"h-4 w-4 animate-spin\" />\n            </IconSlot>\n          ) : null}\n          {state === \"success\" ? (\n            <IconSlot keyId=\"success-icon\">\n              <Check className=\"h-4 w-4\" />\n            </IconSlot>\n          ) : null}\n          {state === \"error\" ? (\n            <IconSlot keyId=\"error-icon\">\n              <X className=\"h-4 w-4\" />\n            </IconSlot>\n          ) : null}\n        </AnimatePresence>\n\n        <TextSlot value={textKey}>{stateText}</TextSlot>\n\n        <AnimatePresence initial={false}>\n          {state === \"idle\" && icon ? (\n            <IconSlot keyId=\"idle-icon\">{icon}</IconSlot>\n          ) : null}\n        </AnimatePresence>\n      </span>\n    </Button>\n  );\n});\n"},{"path":"lib/text-shimmer.ts","type":"registry:lib","target":"@lib/text-shimmer.ts","content":"import type { CSSProperties } from \"react\";\n\nexport const TEXT_SHIMMER_KEYFRAMES =\n  \"@keyframes beui-text-shimmer{from{background-position:200% 0}to{background-position:-200% 0}}\";\n\nexport const TEXT_SHIMMER_CLASS_NAME =\n  \"bg-[length:200%_100%] bg-clip-text text-transparent bg-[linear-gradient(110deg,var(--muted-foreground)_30%,var(--foreground)_50%,var(--muted-foreground)_70%)]\";\n\nexport function textShimmerStyle(duration: number): CSSProperties {\n  return {\n    animation: `beui-text-shimmer ${duration}s linear infinite`,\n  };\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"},{"path":"components/motion/magnetic.tsx","type":"registry:component","target":"@components/motion/magnetic.tsx","content":"\"use client\";\n\nimport { motion, useMotionValue, useReducedMotion, useSpring } from \"motion/react\";\nimport { useRef, type ReactNode } from \"react\";\nimport { SPRING_MOUSE } from \"@/lib/ease\";\nimport { useHoverCapable } from \"@/lib/hooks/use-hover-capable\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface MagneticProps {\n  children: ReactNode;\n  strength?: number;\n  className?: string;\n}\n\nexport function Magnetic({ children, strength = 0.35, className }: MagneticProps) {\n  const ref = useRef<HTMLDivElement>(null);\n  const reduce = useReducedMotion();\n  const canHover = useHoverCapable();\n  // Decorative cursor-follow: skip on touch (phantom hover) and reduced motion.\n  const enabled = !reduce && canHover;\n  const x = useMotionValue(0);\n  const y = useMotionValue(0);\n  const sx = useSpring(x, SPRING_MOUSE);\n  const sy = useSpring(y, SPRING_MOUSE);\n\n  const onMove = (e: React.MouseEvent<HTMLDivElement>) => {\n    const el = ref.current;\n    if (!el || !enabled) return;\n    const rect = el.getBoundingClientRect();\n    x.set((e.clientX - rect.left - rect.width / 2) * strength);\n    y.set((e.clientY - rect.top - rect.height / 2) * strength);\n  };\n\n  const onLeave = () => {\n    x.set(0);\n    y.set(0);\n  };\n\n  return (\n    <motion.div\n      ref={ref}\n      onMouseMove={onMove}\n      onMouseLeave={onLeave}\n      style={{ x: sx, y: sy }}\n      className={cn(\"inline-block\", className)}\n    >\n      {children}\n    </motion.div>\n  );\n}\n"}]}