{"slug":"ai-sidebar","name":"AI Sidebar","description":"A collapsible AI workspace sidebar for folders, projects, files, and bookmarks with keyboard navigation, optimistic moves, inline rename, and overflow-aware labels.","category":"agents","source_url":"https://beui.dev/r/ai-sidebar/raw","detail_url":"https://beui.dev/r/ai-sidebar","raw_url":"https://beui.dev/r/ai-sidebar/raw","page_url":"https://beui.dev/components/agents/ai-sidebar","markdown_url":"https://beui.dev/components/agents/ai-sidebar.md","published_at":"2026-08-02","updated_at":"2026-09-26","dependencies":["clsx","lucide-react","motion","react","react-dom","tailwind-merge"],"internal":["@/components/agents/ai-sidebar","@/components/motion/animated-sidebar","@/components/motion/popover-morph","@/components/motion/popover-position","@/components/motion/shared-layout-bg","@/lib/ease","@/lib/hooks/use-touch-capable","@/lib/utils"],"files":[{"path":"components/agents/ai-sidebar.tsx","type":"component","content":"\"use client\";\n// beui.dev/components/agents/ai-sidebar\n\nimport {\n  ArrowDown,\n  ArrowUp,\n  Bookmark,\n  FileText,\n  Folder,\n  FolderInput,\n  FolderOpen,\n  type LucideIcon,\n  MoreHorizontal,\n  Pencil,\n  Undo2,\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 { useTouchCapable } from \"@/lib/hooks/use-touch-capable\";\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\n/**\n * The moves this row can make right now, the same four the keyboard offers on\n * `Alt+Shift+Arrow`. A pointer drag is the fast path for them; a finger has no\n * drag to give, so the row menu carries them too. Absent keys are moves this\n * row cannot make from where it sits.\n */\nexport interface SidebarResourceMoveCommands {\n  up?: () => void;\n  down?: () => void;\n  into?: { label: string; run: () => void };\n  out?: () => void;\n}\n\nexport interface SidebarResourceMenuControls {\n  close: () => void;\n  rename: () => void;\n  moves: SidebarResourceMoveCommands;\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\nfunction ResourceMenuAction({\n  icon: Icon,\n  onSelect,\n  children,\n}: {\n  icon: LucideIcon;\n  onSelect: () => void;\n  children: ReactNode;\n}) {\n  return (\n    <button\n      type=\"button\"\n      onClick={onSelect}\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      <Icon aria-hidden=\"true\" className=\"size-3.5 shrink-0\" />\n      <span className=\"min-w-0 truncate\">{children}</span>\n    </button>\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  moves: SidebarResourceMoveCommands;\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  moves,\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 canTouch = useTouchCapable();\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 runFromMenu = (action: () => void) => () => {\n    onMenuOpenChange(false);\n    action();\n  };\n\n  const menu = renderMenu?.(row.item, {\n    close: () => onMenuOpenChange(false),\n    rename: () => {\n      onMenuOpenChange(false);\n      onRenameStart();\n    },\n    moves,\n  }) ?? (\n    <>\n      <ResourceMenuAction icon={Pencil} onSelect={runFromMenu(onRenameStart)}>\n        Rename\n      </ResourceMenuAction>\n      {moves.up || moves.down || moves.into || moves.out ? (\n        <div aria-hidden=\"true\" className=\"my-1 h-px bg-border\" />\n      ) : null}\n      {moves.up ? (\n        <ResourceMenuAction icon={ArrowUp} onSelect={runFromMenu(moves.up)}>\n          Move up\n        </ResourceMenuAction>\n      ) : null}\n      {moves.down ? (\n        <ResourceMenuAction icon={ArrowDown} onSelect={runFromMenu(moves.down)}>\n          Move down\n        </ResourceMenuAction>\n      ) : null}\n      {moves.into ? (\n        <ResourceMenuAction\n          icon={FolderInput}\n          onSelect={runFromMenu(moves.into.run)}\n        >\n          Move into {moves.into.label}\n        </ResourceMenuAction>\n      ) : null}\n      {moves.out ? (\n        <ResourceMenuAction icon={Undo2} onSelect={runFromMenu(moves.out)}>\n          Move out\n        </ResourceMenuAction>\n      ) : null}\n    </>\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-muted 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={cn(\n                \"grid size-7 shrink-0 place-items-center rounded-lg 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                // A finger never hovers, and this menu is the only path to\n                // rename and move without a drag — keep it on screen there.\n                canTouch ? \"opacity-100\" : \"opacity-0\",\n              )}\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  // Which row carries the roving tabindex is resolved during render, never in\n  // a passive effect: an effect lands after the browser paints, so the first\n  // commit — and, on a server-rendered page, the markup itself — would have no\n  // tabbable row and Tab would skip the whole tree. The same hole opens again\n  // whenever a collapse or a rolled-back move takes the focused row out of it.\n  const focusedRow =\n    focusedId !== null && flat.some((row) => row.item.id === focusedId)\n      ? focusedId\n      : (flat[0]?.item.id ?? null);\n  if (focusedId !== focusedRow) setFocusedId(focusedRow);\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  // The same four moves `Alt+Shift+Arrow` performs, handed to the row menu so\n  // they survive on a device with no drag and no modifier keys.\n  const moveCommands = useCallback(\n    (row: FlatResource): SidebarResourceMoveCommands => {\n      if (row.item.disabled) return {};\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 parentId = row.parentId;\n      const commands: SidebarResourceMoveCommands = {};\n\n      if (previous) {\n        commands.up = () =>\n          void performMove({\n            itemId: row.item.id,\n            targetId: previous.item.id,\n            position: \"before\",\n          });\n      }\n      if (next) {\n        commands.down = () =>\n          void performMove({\n            itemId: row.item.id,\n            targetId: next.item.id,\n            position: \"after\",\n          });\n      }\n      // Only offer the reparent when it lands somewhere new — the row above a\n      // folder's first child is the folder it already lives in.\n      if (previous && canContain(previous.item) && previous.item.id !== parentId) {\n        commands.into = {\n          label: previous.item.label,\n          run: () => {\n            setExpandedIds((current) => new Set(current).add(previous.item.id));\n            void performMove({\n              itemId: row.item.id,\n              targetId: previous.item.id,\n              position: \"inside\",\n            });\n          },\n        };\n      }\n      if (parentId) {\n        commands.out = () =>\n          void performMove({\n            itemId: row.item.id,\n            targetId: parentId,\n            position: \"after\",\n          });\n      }\n\n      return commands;\n    },\n    [flat, performMove],\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={focusedRow === row.item.id}\n            draggingId={draggingId}\n            dropTarget={dropTarget}\n            menuOpen={menuOpenId === row.item.id}\n            moves={moveCommands(row)}\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-muted 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/motion/popover-morph.tsx","type":"util","content":"\"use client\";\n\nimport {\n  AnimatePresence,\n  motion,\n  animate,\n  useMotionValue,\n  usePresence,\n  useReducedMotion,\n} 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  /** The element the panel measures against — see `registerTrigger`. */\n  triggerRef: React.MutableRefObject<HTMLElement | null>;\n  registerTrigger: (node: HTMLElement | null) => void;\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 [root, setRoot] = useState<HTMLDivElement | null>(null);\n  const [trigger, setTrigger] = useState<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  // A trigger normally registers itself through MorphPopoverTrigger. It can't\n  // when something else already clones the element — a Tooltip wrapping the\n  // button, say — and an unregistered trigger leaves the panel with nothing to\n  // measure against, so it renders permanently invisible. The root boxes the\n  // trigger exactly (the content portals out of it), so it stands in until a\n  // real trigger registers, and stands in again if that one unmounts. Both are\n  // state, so a trigger arriving while the panel is open re-anchors it.\n  const anchorRef = useMemo<React.MutableRefObject<HTMLElement | null>>(\n    () => ({ current: trigger ?? root }),\n    [root, trigger],\n  );\n\n  // The panel is a `role=\"dialog\"` and goes inert the moment it closes, so\n  // focus cannot be left sitting inside it: a dismissal hands it back to the\n  // trigger, the way the ARIA dialog pattern asks. A pointer dismissal takes\n  // the focus onward itself when it lands on something focusable — this only\n  // catches the case where it would otherwise be stranded. When no trigger has\n  // registered, the root anchor stands in only if it can actually hold focus;\n  // there is nowhere better than where the keyboard already is, so leave it.\n  const close = useCallback(() => {\n    setOpen(false);\n    const focused = document.activeElement;\n    const inPanel =\n      focused instanceof HTMLElement && contentRef.current?.contains(focused);\n    if (!inPanel) return;\n    const restore = trigger ?? (root && root.tabIndex >= 0 ? root : null);\n    restore?.focus();\n  }, [root, setOpen, trigger]);\n\n  useEffect(() => {\n    if (!open) return;\n    const onKey = (e: KeyboardEvent) => e.key === \"Escape\" && close();\n    const onPointer = (e: PointerEvent) => {\n      const target = e.target as Node;\n      if (\n        root &&\n        !root.contains(target) &&\n        !contentRef.current?.contains(target)\n      )\n        close();\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, root, close]);\n\n  const ctx = useMemo<MorphContextValue>(\n    () => ({\n      open,\n      setOpen,\n      toggle,\n      triggerId: `${baseId}-trigger`,\n      contentId: `${baseId}-content`,\n      triggerRef: anchorRef,\n      registerTrigger: setTrigger,\n      contentRef,\n    }),\n    [open, setOpen, toggle, baseId, anchorRef],\n  );\n\n  return (\n    <MorphContext.Provider value={ctx}>\n      <div ref={setRoot} 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  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> } | undefined)\n    ?.ref;\n  // Register once per actual ref change, not once per open-state render.\n  const mergedRef = useMemo(\n    () => mergeRefs(childRef, ctx.registerTrigger),\n    [childRef, ctx.registerTrigger],\n  );\n  if (!isValidElement(children)) return children;\n\n  return cloneElement(child, {\n    id: ctx.triggerId,\n    ref: mergedRef,\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 clipAt(side: Side, align: Align, radius: number, inset: number) {\n  const top = side === \"bottom\" ? \"0%\" : `${inset}%`;\n  const bottom = side === \"bottom\" ? `${inset}%` : \"0%\";\n  const right = align === \"end\" ? \"0%\" : `${inset}%`;\n  const left = align === \"end\" ? `${inset}%` : \"0%\";\n  return `inset(${top} ${right} ${bottom} ${left} round ${radius}px)`;\n}\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(props: MorphPopoverContentProps) {\n  const ctx = useMorphContext(\"MorphPopoverContent\");\n  const [portalReady, setPortalReady] = useState(false);\n  useEffect(() => setPortalReady(true), []);\n  if (!portalReady) return null;\n  return createPortal(\n    <AnimatePresence>\n      {ctx.open && <MorphPopoverSurface {...props} />}\n    </AnimatePresence>,\n    document.body,\n  );\n}\n\n// Measurement belongs to the mounted portal session: reopening must not start\n// an entrance at the previous session's coordinates before measuring this one.\nfunction MorphPopoverSurface({\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 [isPresent, safeToRemove] = usePresence();\n  const layout = usePopoverPortalPosition(\n    ctx.triggerRef,\n    ctx.contentRef,\n    isPresent,\n  );\n\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: { scale: 0.96, transition: SPRING_PANEL },\n        show: { scale: 1, transition: SPRING_PANEL },\n      };\n  const clip = reduce\n    ? undefined\n    : {\n        hidden: {\n          clipPath: clipAt(side, align, radius, 92),\n          transition: MORPH_CLIP_TRANSITION,\n        },\n        show: {\n          clipPath: clipAt(side, align, radius, 0),\n          transition: MORPH_CLIP_TRANSITION,\n        },\n      };\n  // Animate the value directly so opacity stays in the inline style throughout\n  // the entrance. A native opacity animation can expose the initial inline 0\n  // for a frame when it finishes, before Motion writes the final value.\n  const opacity = useMotionValue(0);\n  const ready = layout !== null;\n  useEffect(() => {\n    if (!ready) {\n      if (!isPresent) safeToRemove?.();\n      return;\n    }\n    const animation = animate(opacity, isPresent ? 1 : 0, {\n      ...(reduce ? { duration: 0.12 } : SPRING_PANEL),\n      onComplete: () => {\n        if (!isPresent) safeToRemove?.();\n      },\n    });\n    return () => animation.stop();\n  }, [opacity, ready, isPresent, reduce, safeToRemove]);\n\n  return (\n    <motion.div\n      data-morph-popover-portal=\"\"\n      inert={!isPresent}\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=\"hidden\"\n      animate={layout ? \"show\" : \"hidden\"}\n      exit=\"hidden\"\n      style={{\n        left,\n        top,\n        opacity,\n        pointerEvents: isPresent ? \"auto\" : \"none\",\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  );\n}\n"},{"path":"lib/ease.ts","type":"util","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n\n/** Dragged handles and fills (sliders) — critically damped `useSpring` config,\n * so the value follows the pointer butterily and never rebounds off an end. */\nexport const SPRING_GLIDE = {\n  stiffness: 700,\n  damping: 50,\n  mass: 0.5,\n} as const;\n"},{"path":"lib/hooks/use-touch-capable.ts","type":"util","content":"\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\n/**\n * Returns true on devices that can be touched, whatever else they claim.\n *\n * This is not the inverse of `useHoverCapable`: iPadOS Safari browses\n * desktop-class and answers `(hover: hover) and (pointer: fine)` with true\n * while a finger is the only input there is, so anything that treats\n * hover-capable as \"no touch here\" strands every iPad. Gate the *touch path*\n * of an interaction on this hook and leave hover-only polish on\n * `useHoverCapable`, so a component that opens on hover also opens on tap.\n */\nexport function useTouchCapable() {\n  const [canTouch, setCanTouch] = useState(false);\n\n  useEffect(() => {\n    if (typeof window === \"undefined\") return;\n    const mq = window.matchMedia?.(\"(any-pointer: coarse)\");\n    // iPadOS disguises its pointer media queries; maxTouchPoints it reports\n    // honestly, which is what makes it the standard iPad tell.\n    const update = () =>\n      setCanTouch(Boolean(mq?.matches) || navigator.maxTouchPoints > 0);\n    update();\n    mq?.addEventListener?.(\"change\", update);\n    return () => mq?.removeEventListener?.(\"change\", update);\n  }, []);\n\n  return canTouch;\n}\n"},{"path":"lib/utils.ts","type":"util","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/motion/popover-position.ts","type":"util","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/previews/agents/ai-sidebar.preview.tsx","type":"preview","content":"\"use client\";\n\nimport {\n  Clock3,\n  GitPullRequest,\n  LayoutGrid,\n  PanelLeft,\n  Plug,\n  SquarePen,\n} from \"lucide-react\";\nimport { useState } from \"react\";\nimport {\n  AnimatedSidebar,\n  AnimatedSidebarContent,\n  AnimatedSidebarGroup,\n  AnimatedSidebarGroupContent,\n  AnimatedSidebarGroupLabel,\n  AnimatedSidebarInset,\n  AnimatedSidebarMenu,\n  AnimatedSidebarMenuButton,\n  AnimatedSidebarMenuItem,\n  AnimatedSidebarProvider,\n  AnimatedSidebarRail,\n  AnimatedSidebarTrigger,\n} from \"@/components/motion/animated-sidebar\";\nimport {\n  AISidebar,\n  type SidebarResource,\n  type SidebarResourceMove,\n} from \"@/components/agents/ai-sidebar\";\n\nconst resources: SidebarResource[] = [\n  {\n    id: \"design-system\",\n    label: \"Design system\",\n    kind: \"project\",\n  },\n  {\n    id: \"client-portal\",\n    label: \"Client portal\",\n    kind: \"project\",\n  },\n  {\n    id: \"marketing-site\",\n    label: \"Marketing website\",\n    kind: \"project\",\n  },\n  {\n    id: \"platform\",\n    label: \"Platform\",\n    kind: \"project\",\n    children: [\n      { id: \"api\", label: \"API migration\", kind: \"file\" },\n      { id: \"billing\", label: \"Billing states\", kind: \"file\" },\n      { id: \"docs\", label: \"Read platform docs\", kind: \"bookmark\" },\n    ],\n  },\n  {\n    id: \"mobile-app\",\n    label: \"Mobile app\",\n    kind: \"project\",\n  },\n  {\n    id: \"research-lab\",\n    label: \"Research lab\",\n    kind: \"folder\",\n  },\n  {\n    id: \"agent-workspace\",\n    label: \"Agent workspace\",\n    kind: \"project\",\n    children: [\n      {\n        id: \"resource-review\",\n        label: \"Review resource sidebar interaction details\",\n        kind: \"file\",\n      },\n      {\n        id: \"release-offer\",\n        label: \"Prepare release announcement\",\n        kind: \"file\",\n      },\n      {\n        id: \"haptics\",\n        label: \"Explore interaction feedback\",\n        kind: \"bookmark\",\n      },\n      {\n        id: \"promotion\",\n        label: \"Plan launch distribution\",\n        kind: \"file\",\n      },\n      {\n        id: \"motion-research\",\n        label: \"Research motion implementation patterns\",\n        kind: \"bookmark\",\n      },\n    ],\n  },\n  {\n    id: \"release-notes\",\n    label: \"Release notes\",\n    kind: \"file\",\n  },\n];\n\nconst actions = [\n  { label: \"New workspace\", icon: SquarePen },\n  { label: \"Pull requests\", icon: GitPullRequest },\n  { label: \"Sites\", icon: LayoutGrid },\n  { label: \"Scheduled\", icon: Clock3 },\n  { label: \"Extensions\", icon: Plug },\n] as const;\n\nfunction findLabel(items: SidebarResource[], id: string): string | undefined {\n  for (const item of items) {\n    if (item.id === id) return item.label;\n    const child = item.children ? findLabel(item.children, id) : undefined;\n    if (child) return child;\n  }\n}\n\nexport function AISidebarPreview() {\n  const [active, setActive] = useState(\"resource-review\");\n  const [items, setItems] = useState(resources);\n  const activeLabel = findLabel(items, active) ?? active;\n\n  const move = async (_event: SidebarResourceMove) => {\n    await new Promise((resolve) => window.setTimeout(resolve, 450));\n  };\n\n  return (\n    <div className=\"w-full px-0 py-2 sm:p-3\">\n      <AnimatedSidebarProvider\n        style={{ \"--sidebar-width\": \"16rem\" }}\n        className=\"h-[720px] min-h-0 w-full overflow-hidden rounded-2xl border border-foreground/[0.08] bg-background\"\n      >\n        <AnimatedSidebar\n          ariaLabel=\"Workspace resources\"\n          collapsible=\"offcanvas\"\n          className=\"min-h-0 w-full\"\n          panelClassName=\"h-full bg-background\"\n        >\n          <AnimatedSidebarContent className=\"gap-4 overflow-hidden px-2 py-4\">\n            <AnimatedSidebarGroup className=\"shrink-0 px-1 py-0\">\n              <AnimatedSidebarGroupContent>\n                <AnimatedSidebarMenu className=\"gap-1\">\n                  {actions.map(({ label, icon: Icon }) => (\n                    <AnimatedSidebarMenuItem key={label}>\n                      <AnimatedSidebarMenuButton\n                        icon={<Icon className=\"size-4\" />}\n                        onSelect={() => {}}\n                        className=\"font-normal text-foreground\"\n                      >\n                        {label}\n                      </AnimatedSidebarMenuButton>\n                    </AnimatedSidebarMenuItem>\n                  ))}\n                </AnimatedSidebarMenu>\n              </AnimatedSidebarGroupContent>\n            </AnimatedSidebarGroup>\n\n            <AnimatedSidebarGroup className=\"min-h-0 flex-1 px-1 py-0\">\n              <AnimatedSidebarGroupLabel className=\"mb-1 h-8 px-2 text-xs font-medium normal-case tracking-normal\">\n                Projects\n              </AnimatedSidebarGroupLabel>\n              <AnimatedSidebarGroupContent className=\"relative min-h-0 flex-1 overflow-hidden\">\n                <div className=\"h-full overflow-y-auto overscroll-contain pb-8 [overflow-anchor:none] [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\">\n                  <AISidebar\n                    items={items}\n                    activeId={active}\n                    defaultExpandedIds={[\"platform\", \"agent-workspace\"]}\n                    onActiveChange={setActive}\n                    onItemsChange={setItems}\n                    onMove={move}\n                  />\n                </div>\n                <div\n                  aria-hidden=\"true\"\n                  className=\"pointer-events-none absolute inset-x-0 bottom-0 z-20 h-8 bg-gradient-to-t from-background via-background/80 to-transparent\"\n                />\n              </AnimatedSidebarGroupContent>\n            </AnimatedSidebarGroup>\n          </AnimatedSidebarContent>\n          <AnimatedSidebarRail />\n        </AnimatedSidebar>\n\n        <AnimatedSidebarInset className=\"min-h-0 bg-background\">\n          <header className=\"flex h-14 shrink-0 items-center justify-between gap-4 border-border border-b px-5\">\n            <div className=\"flex min-w-0 items-center gap-3\">\n              <AnimatedSidebarTrigger className=\"-ml-2 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground\">\n                <PanelLeft aria-hidden=\"true\" className=\"size-4\" />\n              </AnimatedSidebarTrigger>\n              <div className=\"min-w-0\">\n                <p className=\"text-[10px] font-medium uppercase tracking-[0.14em] text-muted-foreground\">\n                  Workspace\n                </p>\n                <p className=\"truncate text-sm font-medium text-foreground\">\n                  Agent workspace\n                </p>\n              </div>\n            </div>\n            <span className=\"shrink-0 rounded-full bg-muted px-2.5 py-1 text-[10px] font-medium text-muted-foreground\">\n              Draft\n            </span>\n          </header>\n\n          <div className=\"min-h-0 flex-1 overflow-y-auto p-6 sm:p-8\">\n            <div className=\"mx-auto max-w-xl\">\n              <p className=\"text-xs text-muted-foreground\">Selected resource</p>\n              <h3 className=\"mt-2 text-xl font-semibold tracking-tight text-foreground sm:text-2xl\">\n                {activeLabel}\n              </h3>\n              <p className=\"mt-3 max-w-lg text-sm leading-6 text-muted-foreground\">\n                Keep the resource tree predictable while files move between\n                projects, labels overflow, and folders expand around the\n                current selection.\n              </p>\n\n              <div className=\"mt-8 border-border border-t pt-6\">\n                <p className=\"text-sm font-medium text-foreground\">\n                  Interaction notes\n                </p>\n                <ul className=\"mt-3 space-y-3 text-sm leading-6 text-muted-foreground\">\n                  <li>Folders only reveal their contents.</li>\n                  <li>Files can be selected, moved, or renamed.</li>\n                  <li>Rejected moves return to their previous location.</li>\n                </ul>\n              </div>\n            </div>\n          </div>\n        </AnimatedSidebarInset>\n      </AnimatedSidebarProvider>\n    </div>\n  );\n}\n"},{"path":"components/motion/animated-sidebar.tsx","type":"util","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  // The sheet is mounted for as long as the viewport is mobile, so it hides\n  // itself while closed rather than sitting there transparent and interactive.\n  // Opening shows it in the same commit that starts the slide — a delayed show\n  // would run the focus effect below against a still-hidden panel, and focus()\n  // on a hidden element is ignored. Closing waits for the slide to finish, and\n  // the panel's own exit tells us when that is: no duration to keep in sync.\n  const [hidden, setHidden] = useState(!context.openMobile);\n  // The completion callback fires for the open slide too, and it reads state\n  // from whenever motion settles: a ref keeps it on the current one.\n  const openMobileRef = useRef(context.openMobile);\n\n  useEffect(() => setMounted(true), []);\n\n  useEffect(() => {\n    openMobileRef.current = context.openMobile;\n    if (context.openMobile) setHidden(false);\n  }, [context.openMobile]);\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  // This container groups the sheet for hiding and the z-index and carries no\n  // box: both children are `fixed` and resolve against the viewport themselves.\n  // The scrim spans the viewport edges but paints a colour, and the panel is\n  // inset off one side and paints its own surface, so no layer here is a\n  // transparent edge-spanning one.\n  return createPortal(\n    <div\n      className={cn(\n        \"pointer-events-none fixed left-0 top-0 z-50 size-0 md:hidden\",\n        hidden && !context.openMobile ? \"invisible\" : \"visible\",\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          \"fixed 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        onAnimationComplete={() => {\n          if (!openMobileRef.current) setHidden(true);\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 fixed 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    // A submenu cannot render in the icon rail, so opening one from there\n    // leaves its children unreachable — a pointer can still fall back to the\n    // rail or the shortcut, a finger has nothing. Selecting a group unfolds\n    // the panel that is about to hold it.\n    if (ariaExpanded !== undefined && panel.collapsed && !context.isMobile) {\n      context.setOpen(true);\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":"components/motion/shared-layout-bg.tsx","type":"util","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-muted/80\",\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"}]}