{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "workspace-shell",
  "type": "registry:block",
  "title": "Workspace Shell",
  "description": "An inset workspace on beUI: shared chrome, a collapsible sidebar and browser-like tabs joined to the content surface.",
  "author": "asharca",
  "dependencies": [
    "clsx",
    "lucide-react",
    "motion",
    "tailwind-merge"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/workspace/workspace-shell.tsx",
      "type": "registry:component",
      "target": "@components/workspace/workspace-shell.tsx",
      "content": "\"use client\";\n// asharca.github.io/ui/components/blocks/workspace-shell\n\nimport type { ComponentPropsWithRef, ReactNode } from \"react\";\nimport {\n  AnimatedSidebarProvider,\n  type AnimatedSidebarProviderProps,\n} from \"@/components/motion/animated-sidebar\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface WorkspaceShellProps extends AnimatedSidebarProviderProps {\n  sidebar?: ReactNode;\n  tabBar?: ReactNode;\n  mobileHeader?: ReactNode;\n  header?: ReactNode;\n  footer?: ReactNode;\n  /** content: this pane scrolls; none: the editor/chat child owns scrolling. */\n  scroll?: \"content\" | \"none\";\n  contentProps?: Omit<ComponentPropsWithRef<\"div\">, \"children\">;\n}\n\nconst hasSlot = (node: ReactNode) => node != null && typeof node !== \"boolean\";\n\n/** The beUI sidebar provider owns navigation state; the host owns tabs and data.\n * Set a height on the shell. Tabs and content share one alignment line, and the\n * first active tab removes only the adjoining top-left corner of the surface.\n * data-workspace-shell identifies this composition without replacing beUI's\n * own data-slot=\"sidebar-wrapper\" or introducing a second layout wrapper.\n */\nexport function WorkspaceShell({\n  sidebar, tabBar, mobileHeader, header, footer, children, className, style,\n  scroll = \"content\", contentProps, ...props\n}: WorkspaceShellProps) {\n  const sidebarInset = hasSlot(sidebar) && \"md:ml-[var(--workspace-gap,0px)]\";\n  return (\n    <AnimatedSidebarProvider\n      {...props}\n      data-workspace-shell=\"\"\n      style={{ \"--sidebar-width\": \"14rem\", \"--sidebar-width-icon\": \"4rem\", ...style }}\n      className={cn(\"h-full min-h-0 w-full overflow-hidden bg-[var(--workspace-shell-background,var(--muted))] text-foreground\", className)}\n    >\n      {sidebar}\n      <div data-slot=\"workspace-body\" className=\"flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden\">\n        {hasSlot(mobileHeader) && <div data-slot=\"workspace-mobile-header\" className=\"flex h-12 shrink-0 items-center gap-2 px-2 md:hidden\">{mobileHeader}</div>}\n        {hasSlot(tabBar) && <div data-slot=\"workspace-shell-tabs\" className={cn(\"peer/workspace-tabs mx-[var(--workspace-gap,0.5rem)] min-w-0 shrink-0\", sidebarInset)}>{tabBar}</div>}\n        <div data-slot=\"workspace-surface\" className={cn(\n          \"m-[var(--workspace-gap,0.5rem)] flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden rounded-[var(--workspace-radius,0.75rem)] bg-[var(--workspace-surface,var(--background))]\",\n          sidebarInset,\n          hasSlot(tabBar) && \"mt-0 peer-has-[[data-leading-active=true]]/workspace-tabs:rounded-tl-none\",\n        )}>\n          {hasSlot(header) && <div data-slot=\"workspace-header\" className=\"min-w-0 shrink-0\">{header}</div>}\n          <div {...contentProps} data-slot=\"workspace-content\" data-scroll={scroll} className={cn(\"min-h-0 min-w-0 flex-1\", scroll === \"content\" ? \"overflow-auto overscroll-contain\" : \"flex flex-col overflow-hidden\", contentProps?.className)}>{children}</div>\n          {hasSlot(footer) && <div data-slot=\"workspace-footer\" className=\"min-w-0 shrink-0\">{footer}</div>}\n        </div>\n      </div>\n    </AnimatedSidebarProvider>\n  );\n}\n\n/** Invoke synchronously from a user action. null means the browser blocked it.\n * Only the host decides whether to remove a tab and what state may be copied.\n */\nexport function openWorkspaceWindow(href: string | URL, prepare?: (popup: Window) => void): Window | null {\n  const url = new URL(href, window.location.href);\n  if (![\"http:\", \"https:\"].includes(url.protocol) || url.origin !== window.location.origin || url.username || url.password) {\n    throw new Error(\"工作区窗口必须使用不含凭据的同源 HTTP(S) 地址。\");\n  }\n  const popup = window.open(\"about:blank\", \"_blank\", \"popup,width=1100,height=760\");\n  if (!popup) return null;\n  try {\n    popup.opener = null;\n    prepare?.(popup);\n    popup.location.replace(url.href);\n    return popup;\n  } catch (error) {\n    popup.close();\n    throw error;\n  }\n}\n"
    },
    {
      "path": "components/workspace/workspace-tab-bar.tsx",
      "type": "registry:component",
      "target": "@components/workspace/workspace-tab-bar.tsx",
      "content": "\"use client\";\n// asharca.github.io/ui/components/blocks/workspace-shell\n\nimport { ArrowLeft, ArrowRight, ExternalLink, MoreHorizontal, Pin, PinOff, Plus, X } from \"lucide-react\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { useCallback, useEffect, useId, useLayoutEffect, useRef, useState, type KeyboardEvent as ReactKeyboardEvent, type MouseEvent as ReactMouseEvent, type PointerEvent as ReactPointerEvent, type ReactNode } from \"react\";\nimport { Button } from \"@/components/motion/button/base\";\nimport { MorphPopover, MorphPopoverTrigger, MorphPopoverContent } from \"@/components/motion/popover-morph\";\nimport { SPRING_LAYOUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\ntype MenuPoint = { x: number; y: number };\nconst LONG_PRESS_DELAY = 520;\nconst LONG_PRESS_TOLERANCE = 10;\n\nexport interface WorkspaceTab {\n  id: string;\n  title: string;\n  icon?: ReactNode;\n  pinned?: boolean;\n  dirty?: boolean;\n  closable?: boolean;\n  detachable?: boolean;\n  tabId?: string;\n  panelId?: string;\n}\nexport interface WorkspaceTabBarProps {\n  tabs: WorkspaceTab[];\n  activeTabId: string;\n  onSelect: (id: string) => void;\n  onClose?: (id: string) => void;\n  onReorder?: (sourceId: string, targetId: string) => void;\n  onPinnedChange?: (id: string, pinned: boolean) => void;\n  onOpenInNewWindow?: (id: string) => void;\n  onNewTab?: () => void;\n  actions?: ReactNode;\n  closeOnMiddleClick?: boolean;\n  closeOnDoubleClick?: boolean;\n  className?: string;\n}\ninterface TabAction { key: string; label: string; icon: ReactNode; disabled?: boolean; run: () => void }\n\n/** Per-tab action menu. One beUI MorphPopover serves every invocation: the ⋯\n *  button, right-click, long-press and Shift+F10 all open this same panel.\n *  `openAt` positions without moving focus; a real trigger click just toggles. */\nfunction TabActions({ title, items, restore, openRequest, onOpenHandled, anchorTabId }: {\n  title: string;\n  items: TabAction[];\n  restore: () => void;\n  openRequest: { point: MenuPoint; invocation: number } | null;\n  onOpenHandled: () => void;\n  anchorTabId: string;\n}) {\n  const [open, setOpen] = useState(false);\n  const panel = useRef<HTMLDivElement>(null);\n  const popover = useRef<HTMLDivElement>(null);\n  const anchor = useRef<HTMLElement | null>(null);\n  useLayoutEffect(() => {\n    if (!open) return;\n    // beUI portals mount in an effect. Wait one frame for the actual actions.\n    const frame = requestAnimationFrame(() => panel.current?.querySelector<HTMLButtonElement>(\"button:not(:disabled)\")?.focus());\n    return () => cancelAnimationFrame(frame);\n  }, [open]);\n  useLayoutEffect(() => {\n    if (!openRequest) return;\n    anchor.current = popover.current?.querySelector<HTMLElement>(`[data-tab-menu-anchor=\"${anchorTabId}\"]`) ?? null;\n    setOpen(false);\n    setOpen(true);\n    onOpenHandled();\n  }, [openRequest, anchorTabId, onOpenHandled]);\n  return <span ref={popover} className=\"contents\">\n    <MorphPopover open={open} onOpenChange={(next) => { setOpen(next); if (!next) restore(); }}>\n      {!openRequest && <MorphPopoverTrigger><Button variant=\"ghost\" size=\"icon\" className=\"size-6 rounded-md\" aria-label={`${title}操作`}><MoreHorizontal className=\"size-3.5\" /></Button></MorphPopoverTrigger>}\n      <MorphPopoverContent role=\"menu\" align=\"end\" radius={10} className=\"w-48 p-1\">\n        <div ref={panel} inert={!open}>\n          {items.map((item) => <Button key={item.key} role=\"menuitem\" variant=\"ghost\" size=\"sm\" disabled={item.disabled} className=\"w-full justify-start rounded-md px-2 text-xs\"\n            onClick={() => { item.run(); setOpen(false); restore(); }}>{item.icon}{item.label}</Button>)}\n        </div>\n      </MorphPopoverContent>\n    </MorphPopover>\n  </span>;\n}\n\n/** Browser-like tabs joined to the content surface. Only the selected surface\n * moves; controls stay in place. Selection/order/dirty confirmation are host-owned.\n */\ninterface TabMenuHostProps {\n  tab: WorkspaceTab;\n  index: number;\n  selected: boolean;\n  items: TabAction[];\n  instance: string;\n  tabs: WorkspaceTab[];\n  refs: React.MutableRefObject<Map<string, HTMLButtonElement>>;\n  onSelect: (id: string) => void;\n  close: (tab: WorkspaceTab) => void;\n  closeOnMiddleClick: boolean;\n  closeOnDoubleClick: boolean;\n  onReorder: boolean;\n  move: (tab: WorkspaceTab, target?: WorkspaceTab) => void;\n  selectAt: (index: number) => void;\n  restore: (id: string) => void;\n  canClose: (tab: WorkspaceTab) => boolean;\n  onClose: boolean;\n}\n\n/** One tab plus its sibling controls. Right-click, touch long-press and\n *  Shift+F10/CopyAll open the same MorphPopover as the ⋯ button — the\n *  ContextMenu copy is gone, so there is a single menu to maintain. */\nfunction TabMenuHost({ tab, index, selected, items, instance, tabs, refs, onSelect, close, closeOnMiddleClick, closeOnDoubleClick, onReorder, move, selectAt, restore, canClose, onClose }: TabMenuHostProps) {\n  const reduce = useReducedMotion();\n  const longPressTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n  const touchOrigin = useRef<MenuPoint | null>(null);\n  const [openRequest, setOpenRequest] = useState<{ point: MenuPoint; invocation: number } | null>(null);\n\n  const cancelLongPress = useCallback(() => {\n    if (longPressTimer.current) {\n      clearTimeout(longPressTimer.current);\n      longPressTimer.current = null;\n    }\n    touchOrigin.current = null;\n  }, []);\n  useEffect(() => cancelLongPress, [cancelLongPress]);\n\n  const openMenuAt = useCallback((point: MenuPoint) => {\n    setOpenRequest((current) => ({ point, invocation: (current?.invocation ?? 0) + 1 }));\n  }, []);\n\n  const onContextMenu = (event: ReactMouseEvent<HTMLElement>) => {\n    if (!items.length) return;\n    event.preventDefault();\n    cancelLongPress();\n    openMenuAt({ x: event.clientX, y: event.clientY });\n  };\n  // Touch/pen: hold to open, like the platform context menu. A drag cancels.\n  const onPointerDown = (event: ReactPointerEvent<HTMLElement>) => {\n    if (!items.length) return;\n    const pressToOpen = event.pointerType === \"touch\" || event.pointerType === \"pen\";\n    if (pressToOpen) {\n      const origin = { x: event.clientX, y: event.clientY };\n      touchOrigin.current = origin;\n      longPressTimer.current = setTimeout(() => {\n        longPressTimer.current = null;\n        touchOrigin.current = null;\n        openMenuAt(origin);\n      }, LONG_PRESS_DELAY);\n    }\n  };\n  const onPointerMove = (event: ReactPointerEvent<HTMLElement>) => {\n    const origin = touchOrigin.current;\n    if (origin && Math.hypot(event.clientX - origin.x, event.clientY - origin.y) > LONG_PRESS_TOLERANCE) cancelLongPress();\n  };\n  // Keyboard: Menu key or Shift+F10 opens at the tab, matching ContextMenu's contract.\n  const onKeyDown = (event: ReactKeyboardEvent<HTMLElement>) => {\n    if (event.key === \"Delete\") { event.preventDefault(); close(tab); }\n    if (event.key === \"ContextMenu\" || (event.shiftKey && event.key === \"F10\")) {\n      if (!items.length) return;\n      event.preventDefault();\n      const rect = event.currentTarget.getBoundingClientRect();\n      openMenuAt({ x: rect.left + Math.min(24, rect.width / 2), y: rect.top + rect.height / 2 });\n    }\n    if (event.key === \"ArrowLeft\" || event.key === \"ArrowRight\") {\n      event.preventDefault();\n      const step = (event.key === \"ArrowLeft\" ? -1 : 1) * (getComputedStyle(event.currentTarget).direction === \"rtl\" ? -1 : 1);\n      if (event.altKey) move(tab, tabs[index + step]); else selectAt(index + step);\n    }\n    if (event.key === \"Home\" || event.key === \"End\") { event.preventDefault(); selectAt(event.key === \"Home\" ? 0 : tabs.length - 1); }\n  };\n\n  return (\n    /* biome-ignore lint/a11y/useSemanticElements: This groups a tab and sibling actions, not form fields. */\n    <div role=\"group\" aria-label={`${tab.title}标签及操作`} data-slot=\"workspace-tab\" data-tab-key={tab.id} data-active={selected} tabIndex={-1}\n      draggable={onReorder}\n      onContextMenu={onContextMenu}\n      onPointerDown={onPointerDown}\n      onPointerMove={onPointerMove}\n      onPointerUp={cancelLongPress}\n      onPointerCancel={cancelLongPress}\n      onKeyDown={onKeyDown}\n      onDragStart={(event) => { event.dataTransfer.setData(\"text/plain\", `${instance}:${tab.id}`); event.dataTransfer.effectAllowed = \"move\"; }}\n      onDragOver={(event) => { if (onReorder) event.preventDefault(); }}\n      onDrop={(event) => { event.preventDefault(); const key = event.dataTransfer.getData(\"text/plain\"); const source = tabs.find((item) => `${instance}:${item.id}` === key); if (source) move(source, tab); }}\n      className=\"group relative isolate flex h-10 shrink-0 items-center rounded-t-[var(--workspace-radius,0.75rem)] px-1 pb-0.5 outline-none\">\n      {selected && <motion.span aria-hidden=\"true\" data-slot=\"workspace-tab-selection\" initial={false}\n        layoutId={reduce ? undefined : `${instance}-surface`} transition={reduce ? { duration: 0 } : SPRING_LAYOUT}\n        className={cn(\"pointer-events-none absolute inset-0 -z-10 rounded-t-[var(--workspace-radius,0.75rem)] bg-[var(--workspace-surface,var(--background))] after:absolute after:-right-2 after:bottom-0 after:size-2 after:bg-[radial-gradient(circle_at_top_right,transparent_70%,var(--workspace-surface,var(--background))_72%)]\", index > 0 && \"before:absolute before:-left-2 before:bottom-0 before:size-2 before:bg-[radial-gradient(circle_at_top_left,transparent_70%,var(--workspace-surface,var(--background))_72%)]\")} />}\n      <button ref={(node) => { if (node) refs.current.set(tab.id, node); else refs.current.delete(tab.id); }} type=\"button\" role=\"tab\"\n        id={tab.tabId} aria-controls={tab.panelId} aria-selected={selected} tabIndex={selected ? 0 : -1}\n        onClick={() => onSelect(tab.id)}\n        onAuxClick={(event) => { if (event.button === 1 && closeOnMiddleClick) { event.preventDefault(); close(tab); } }}\n        onDoubleClick={() => { if (closeOnDoubleClick) close(tab); }}\n        className=\"inline-flex h-8 max-w-48 items-center gap-2 rounded-md px-2.5 text-xs text-muted-foreground outline-none aria-selected:text-foreground focus-visible:ring-2 focus-visible:ring-ring\">\n        {tab.icon && <span aria-hidden=\"true\" className=\"inline-flex shrink-0 [&_svg]:size-3.5\">{tab.icon}</span>}\n        {tab.pinned && <Pin aria-hidden=\"true\" className=\"size-3 shrink-0\" />}\n        <span className=\"truncate\" title={tab.title}>{tab.title}</span>\n        {tab.dirty && <><span aria-hidden=\"true\" className=\"size-1.5 shrink-0 rounded-full bg-current\" /><span className=\"sr-only\">（未保存）</span></>}\n      </button>\n      {items.length > 0 && <TabActions title={tab.title} items={items} restore={() => restore(tab.id)} openRequest={openRequest} onOpenHandled={() => setOpenRequest(null)} anchorTabId={tab.id} />}\n      {onClose && !tab.pinned && <Button variant=\"ghost\" size=\"icon\" className=\"mr-1 size-6 rounded-md\" aria-label={`关闭 ${tab.title}`} disabled={!canClose(tab)} onClick={() => close(tab)}><X aria-hidden=\"true\" className=\"size-3.5\" /></Button>}\n    </div>\n  );\n}\n\nexport function WorkspaceTabBar({ tabs, activeTabId, onSelect, onClose, onReorder, onPinnedChange, onOpenInNewWindow, onNewTab, actions, closeOnMiddleClick = true, closeOnDoubleClick = false, className }: WorkspaceTabBarProps) {\n  const instance = useId();\n  const refs = useRef(new Map<string, HTMLButtonElement>());\n  const scroller = useRef<HTMLDivElement>(null);\n  const closing = useRef<string | null>(null);\n  const currentId = tabs.some((tab) => tab.id === activeTabId) ? activeTabId : tabs[0]?.id;\n  const canClose = (tab: WorkspaceTab) => Boolean(onClose && !tab.pinned && tab.closable !== false && tabs.length > 1);\n  const restore = (id: string) => (refs.current.get(id) ?? refs.current.get(currentId) ?? refs.current.get(tabs[0]?.id))?.focus({ preventScroll: true });\n  useLayoutEffect(() => {\n    if (closing.current && !tabs.some((tab) => tab.id === closing.current)) {\n      closing.current = null;\n      (refs.current.get(currentId) ?? refs.current.get(tabs[0]?.id))?.focus({ preventScroll: true });\n    }\n    const node = scroller.current;\n    const active = refs.current.get(currentId)?.closest<HTMLElement>(\"[data-slot='workspace-tab']\");\n    if (!node || !active) return;\n    const bounds = node.getBoundingClientRect();\n    const rect = active.getBoundingClientRect();\n    if (rect.left < bounds.left) node.scrollLeft += rect.left - bounds.left;\n    else if (rect.right > bounds.right) node.scrollLeft += rect.right - bounds.right;\n  }, [currentId, tabs]);\n  const close = (tab: WorkspaceTab) => { if (canClose(tab)) { closing.current = tab.id; onClose?.(tab.id); } };\n  const move = (tab: WorkspaceTab, target?: WorkspaceTab) => {\n    if (!target || tab.id === target.id || Boolean(tab.pinned) !== Boolean(target.pinned)) return;\n    onReorder?.(tab.id, target.id);\n    restore(tab.id);\n  };\n  const selectAt = (index: number) => {\n    const target = tabs[(index + tabs.length) % tabs.length];\n    if (target) { onSelect(target.id); restore(target.id); }\n  };\n  const menuItems = (tab: WorkspaceTab, index: number) => {\n    const items: TabAction[] = [];\n    if (onOpenInNewWindow) items.push({ key: \"detach\", label: \"在独立窗口打开\", icon: <ExternalLink className=\"size-3.5\" />, disabled: tab.detachable === false, run: () => onOpenInNewWindow(tab.id) });\n    if (onPinnedChange) items.push({ key: \"pin\", label: tab.pinned ? \"取消固定\" : \"固定标签\", icon: tab.pinned ? <PinOff className=\"size-3.5\" /> : <Pin className=\"size-3.5\" />, run: () => onPinnedChange(tab.id, !tab.pinned) });\n    if (onReorder) {\n      items.push({ key: \"left\", label: \"向左移动\", icon: <ArrowLeft className=\"size-3.5\" />, disabled: !tabs[index - 1] || Boolean(tabs[index - 1].pinned) !== Boolean(tab.pinned), run: () => move(tab, tabs[index - 1]) });\n      items.push({ key: \"right\", label: \"向右移动\", icon: <ArrowRight className=\"size-3.5\" />, disabled: !tabs[index + 1] || Boolean(tabs[index + 1].pinned) !== Boolean(tab.pinned), run: () => move(tab, tabs[index + 1]) });\n    }\n    if (onClose) items.push({ key: \"close\", label: \"关闭标签\", icon: <X className=\"size-3.5\" />, disabled: !canClose(tab), run: () => close(tab) });\n    return items;\n  };\n  return <div data-slot=\"workspace-tab-strip\" data-leading-active={tabs[0]?.id === currentId} className={cn(\"flex h-11 max-w-full min-w-0 shrink-0 items-end\", className)}>\n    <motion.div layoutScroll ref={scroller} data-slot=\"workspace-tab-bar\" className=\"min-w-0 flex-1 overflow-x-auto overscroll-x-contain [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\">\n      {/* Each tab owns sibling action buttons; never put controls inside a tab button. */}\n      <div role=\"tablist\" aria-label=\"工作区标签\" className=\"flex h-11 min-w-max items-end gap-1 pt-1\">\n        {tabs.map((tab, index) => {\n          const selected = tab.id === currentId;\n          const items = menuItems(tab, index);\n          return <TabMenuHost key={tab.id} tab={tab} index={index} selected={selected} items={items} instance={instance} tabs={tabs}\n            refs={refs} onSelect={onSelect} close={close} closeOnMiddleClick={closeOnMiddleClick}\n            closeOnDoubleClick={closeOnDoubleClick} onReorder={Boolean(onReorder)} move={move} selectAt={selectAt}\n            restore={restore} canClose={canClose} onClose={Boolean(onClose)} />;\n        })}\n      </div>\n    </motion.div>\n    {(onNewTab || actions) && <div data-slot=\"workspace-tab-actions\" className=\"flex h-11 shrink-0 items-center gap-1 pl-1\">\n      {onNewTab && <Button variant=\"ghost\" size=\"icon\" aria-label=\"新建标签\" onClick={onNewTab}><Plus className=\"size-4\" /></Button>}{actions}\n    </div>}\n  </div>;\n}\n"
    },
    {
      "path": "components/workspace/workspace-sidebar.tsx",
      "type": "registry:component",
      "target": "@components/workspace/workspace-sidebar.tsx",
      "content": "\"use client\";\n// asharca.github.io/ui/components/blocks/workspace-shell\n\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { PanelLeft, X } from \"lucide-react\";\nimport { useLayoutEffect, useRef, useState, type ReactNode } from \"react\";\nimport {\n  AnimatedSidebar, AnimatedSidebarClose, AnimatedSidebarContent,\n  AnimatedSidebarFooter, AnimatedSidebarGroup,\n  AnimatedSidebarHeader, AnimatedSidebarMenu, AnimatedSidebarMenuButton,\n  AnimatedSidebarMenuItem, AnimatedSidebarMenuSub, AnimatedSidebarMenuSubButton,\n  AnimatedSidebarMenuSubItem, AnimatedSidebarTrigger, useAnimatedSidebar,\n} from \"@/components/motion/animated-sidebar\";\nimport { EASE_OUT, SPRING_LAYOUT } from \"@/lib/ease\";\nimport { useHoverCapable } from \"@/lib/hooks/use-hover-capable\";\nimport { useOnOpen } from \"@/lib/hooks/use-on-open\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface WorkspaceSidebarItem {\n  id: string;\n  label: string;\n  icon?: ReactNode;\n  badge?: ReactNode;\n  href?: string;\n  disabled?: boolean;\n  /** Exactly one nested level. Parents toggle their children instead of navigating. */\n  children?: Omit<WorkspaceSidebarItem, \"children\">[];\n}\nexport interface WorkspaceSidebarGroup { id: string; title?: string; items: WorkspaceSidebarItem[] }\nexport interface WorkspaceSidebarProps {\n  groups: WorkspaceSidebarGroup[];\n  activeId?: string;\n  onSelect?: (id: string) => void;\n  title?: string;\n  /** Brand mark rendered in the header instead of the plain title text. */\n  logo?: ReactNode;\n  footer?: ReactNode;\n  className?: string;\n}\n\nfunction WorkspaceNavigationItem({ item, activeId, onSelect, compact }: {\n  item: WorkspaceSidebarItem;\n  activeId?: string;\n  onSelect?: (id: string) => void;\n  compact: boolean;\n}) {\n  const children = item.children?.length ? item.children : undefined;\n  const activeChild = children?.some((child) => child.id === activeId) ?? false;\n  const [expanded, setExpanded] = useState(activeChild);\n  const [previousActive, setPreviousActive] = useState(activeId);\n  if (previousActive !== activeId) {\n    setPreviousActive(activeId);\n    if (activeChild) setExpanded(true);\n  }\n  return <AnimatedSidebarMenuItem>\n    <AnimatedSidebarMenuButton\n      isActive={activeId === item.id || (compact && activeChild)}\n      ariaExpanded={children ? !compact && expanded : undefined}\n      onSelect={() => children ? setExpanded((current) => compact || !current) : onSelect?.(item.id)}\n      href={children ? undefined : item.href} icon={item.icon} badge={item.badge} disabled={item.disabled}\n      className={cn(\"min-h-9 gap-2 overflow-visible rounded-lg px-2 text-xs [&>span:first-child:not([aria-hidden])]:rounded-lg [&>span[aria-hidden=true]]:size-5 [&_svg]:size-[18px] [&>span.absolute]:bg-[var(--workspace-surface,var(--background))]\", activeChild && !compact && \"bg-foreground/5 text-foreground\")}\n    >\n      {item.label}\n    </AnimatedSidebarMenuButton>\n    {children && <div inert={compact || !expanded || item.disabled} aria-hidden={compact || !expanded || item.disabled}>\n      <AnimatedSidebarMenuSub open={expanded} className=\"ml-4 gap-1 border-foreground/10 pl-3\">\n        {children.map((child) => <AnimatedSidebarMenuSubItem key={child.id}>\n          <AnimatedSidebarMenuSubButton href={child.href} icon={child.icon}\n            disabled={item.disabled || child.disabled} isActive={activeId === child.id}\n            onSelect={() => onSelect?.(child.id)}\n            className={cn(\"min-h-9 [&_svg]:size-3.5\", activeId === child.id && \"bg-[var(--workspace-surface,var(--background))] font-medium hover:bg-[var(--workspace-surface,var(--background))]\")}\n          >\n            <span className=\"flex min-w-0 items-center gap-2\"><span className=\"truncate\">{child.label}</span>{child.badge != null && <span className=\"ml-auto shrink-0 rounded bg-muted px-1 text-[10px] text-muted-foreground\">{child.badge}</span>}</span>\n          </AnimatedSidebarMenuSubButton>\n        </AnimatedSidebarMenuSubItem>)}\n      </AnimatedSidebarMenuSub>\n    </div>}\n  </AnimatedSidebarMenuItem>;\n}\n\n/** Composition of beUI primitives, not a second copy of the sidebar engine.\n *  The footer stays mounted and interactive while collapsed: hosts put their\n *  icon-sized workspace/avatar popovers there and manage their own visibility. */\nexport function WorkspaceSidebar({ groups, activeId, onSelect, title = \"工作区\", logo, footer, className }: WorkspaceSidebarProps) {\n  const { open, isMobile } = useAnimatedSidebar();\n  const reduced = useReducedMotion() ?? false;\n  const compact = !open && !isMobile;\n  const canHover = useHoverCapable();\n  const [hovered, setHovered] = useState(false);\n  const [focused, setFocused] = useState(false);\n  useOnOpen(compact, () => setHovered(false));\n  const showToggle = !compact || !logo || (canHover && hovered) || focused;\n  const toggleRef = useRef<HTMLButtonElement>(null);\n  useLayoutEffect(() => {\n    if (compact && document.activeElement instanceof HTMLElement) {\n      const panel = toggleRef.current?.closest('[data-slot=\"sidebar\"]');\n      if (panel?.getAttribute(\"data-state\") === \"collapsed\" && panel.contains(document.activeElement)) {\n        const footer = panel.querySelector('[data-slot=\"sidebar-footer\"]');\n        if (!(footer instanceof HTMLElement && footer.contains(document.activeElement))) toggleRef.current?.focus();\n      }\n    }\n  }, [compact]);\n  // Opacity must not inherit spring velocity when hover reverses mid-flight.\n  const transition = reduced ? { duration: 0 } : {\n    ...SPRING_LAYOUT,\n    opacity: { type: \"tween\" as const, duration: 0.14, ease: EASE_OUT },\n  };\n  return <AnimatedSidebar ariaLabel={`${title}导航`} variant=\"inset\"\n    className={cn(!isMobile && \"h-full\", className)} panelClassName=\"static m-0 h-full rounded-none bg-transparent\">\n    <AnimatedSidebarHeader className=\"px-3.5 pt-2 pb-0\">\n      <motion.div\n        initial={false}\n        animate={{ height: compact ? 36 : 64 }}\n        transition={transition}\n        onMouseEnter={() => setHovered(true)}\n        onMouseLeave={() => setHovered(false)}\n        onFocusCapture={(event) => setFocused(event.target.matches(\":focus-visible\"))}\n        onBlurCapture={() => setFocused(false)}\n        className=\"relative flex w-full flex-row items-center\"\n      >\n        {isMobile ? (\n          <>\n            <span className=\"flex min-w-0 flex-1 items-center gap-2 text-xs font-semibold\">\n              {logo && <span className=\"grid size-7 shrink-0 place-items-center\">{logo}</span>}\n              <span className=\"truncate\">{title}</span>\n            </span>\n            <AnimatedSidebarClose aria-label=\"关闭工作区导航\" className=\"size-9 shrink-0\"><X className=\"size-4\" /></AnimatedSidebarClose>\n          </>\n        ) : (\n          <>\n            {logo && <motion.span\n              initial={false}\n              animate={{ y: compact && showToggle && !reduced ? 20 : 0, opacity: compact && showToggle ? 0 : 1 }}\n              transition={transition}\n              aria-hidden=\"true\"\n              className=\"pointer-events-none absolute left-0 grid size-9 place-items-center\"\n            >\n              <span className=\"grid size-7 place-items-center\">{logo}</span>\n            </motion.span>}\n            {/* The reveal edge and trigger share the sidebar's actual width.\n                No measured-width spring chasing the sidebar's own spring. */}\n            <div aria-hidden={compact} className=\"pointer-events-none absolute inset-y-0 left-0 right-9 overflow-hidden\">\n              <motion.span\n                initial={false}\n                animate={{ x: compact && !reduced ? -8 : 0 }}\n                transition={transition}\n                className={cn(\"flex h-full items-center whitespace-nowrap text-xs font-semibold\", logo ? \"pl-11\" : \"pl-2\")}\n              >\n                {title}\n              </motion.span>\n            </div>\n            <AnimatedSidebarTrigger\n              ref={toggleRef}\n              aria-label={compact ? \"展开工作区侧栏\" : \"折叠工作区侧栏\"}\n              className=\"absolute right-0 z-10 size-9 overflow-hidden rounded-lg focus-visible:ring-inset\"\n            >\n              <motion.span\n                initial={false}\n                animate={{ x: showToggle || reduced ? 0 : 36, opacity: showToggle ? 1 : 0 }}\n                transition={transition}\n                className=\"grid size-9 shrink-0 place-items-center\"\n              >\n                <PanelLeft className=\"size-[18px]\" />\n              </motion.span>\n            </AnimatedSidebarTrigger>\n          </>\n        )}\n      </motion.div>\n    </AnimatedSidebarHeader>\n    <AnimatedSidebarContent className=\"gap-6 px-3.5 py-3 [scrollbar-width:none]\">\n      {groups.map((group) => <AnimatedSidebarGroup key={group.id} role=\"group\" aria-label={group.title} className=\"px-0 py-0\">\n        <AnimatedSidebarMenu className=\"gap-1\">\n          {group.items.map((item) => <WorkspaceNavigationItem key={item.id} item={item} activeId={activeId} onSelect={onSelect} compact={compact} />)}\n        </AnimatedSidebarMenu>\n      </AnimatedSidebarGroup>)}\n    </AnimatedSidebarContent>\n    {footer && <AnimatedSidebarFooter className=\"mx-3.5 w-auto border-t border-border/60 px-0 pt-3 pb-3\">{footer}</AnimatedSidebarFooter>}\n  </AnimatedSidebar>;\n}\n"
    },
    {
      "path": "components/motion/animated-sidebar.tsx",
      "type": "registry:component",
      "target": "@components/motion/animated-sidebar.tsx",
      "content": "\"use client\";\n\nimport { ChevronRight } from \"lucide-react\";\nimport {\n  AnimatePresence,\n  type HTMLMotionProps,\n  motion,\n  useReducedMotion,\n  type Variants,\n} from \"motion/react\";\nimport {\n  type ButtonHTMLAttributes,\n  type CSSProperties,\n  createContext,\n  forwardRef,\n  type HTMLAttributes,\n  type ReactNode,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n  useSyncExternalStore,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { SharedLayoutBg } from \"@/components/motion/shared-layout-bg\";\nimport {\n  EASE_DRAWER,\n  EASE_OUT,\n  SPRING_LAYOUT,\n  SPRING_PRESS,\n} from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\ntype SidebarState = \"expanded\" | \"collapsed\";\ntype SidebarSide = \"left\" | \"right\";\ntype SidebarVariant = \"sidebar\" | \"floating\" | \"inset\";\ntype SidebarCollapsible = \"offcanvas\" | \"icon\" | \"none\";\n\nconst MOBILE_QUERY = \"(max-width: 767px)\";\nconst SIDEBAR_KEYBOARD_SHORTCUT = \"b\";\n\nconst PANEL_TRANSITION = {\n  duration: 0.36,\n  ease: EASE_DRAWER,\n} as const;\n\n// The desktop rail settles at a hard zero-width boundary. Keep the spring\n// critically damped so it cannot overshoot, pause against that boundary, and\n// then snap back during the final frame.\nconst SIDEBAR_MORPH_TRANSITION = {\n  type: \"spring\",\n  stiffness: 380,\n  damping: 35,\n  mass: 0.75,\n} as const;\n\nconst LABEL_ENTER_TRANSITION = {\n  duration: 0.2,\n  delay: 0.08,\n  ease: EASE_OUT,\n} as const;\n\nconst LABEL_EXIT_TRANSITION = {\n  duration: 0.12,\n  ease: EASE_OUT,\n} as const;\n\nconst SUBMENU_TRANSITION = {\n  duration: 0.18,\n  ease: EASE_OUT,\n} as const;\n\nconst SUBMENU_VARIANTS: Variants = {\n  closed: {\n    opacity: 0,\n    clipPath: \"inset(0 0 100% 0 round 8px)\",\n    transition: {\n      duration: 0.14,\n      ease: EASE_OUT,\n      staggerChildren: 0.025,\n      staggerDirection: -1,\n    },\n  },\n  open: {\n    opacity: 1,\n    clipPath: \"inset(0 0 0% 0 round 8px)\",\n    transition: {\n      duration: 0.2,\n      delayChildren: 0.035,\n      ease: EASE_OUT,\n      staggerChildren: 0.045,\n    },\n  },\n};\n\nconst SUBMENU_ITEM_VARIANTS: Variants = {\n  closed: {\n    opacity: 0,\n    y: -6,\n    filter: \"blur(3px)\",\n  },\n  open: {\n    opacity: 1,\n    y: 0,\n    filter: \"blur(0px)\",\n    transition: SUBMENU_TRANSITION,\n  },\n};\n\nconst REDUCED_TRANSITION = {\n  duration: 0.16,\n  ease: EASE_OUT,\n} as const;\n\nconst FOCUSABLE_SELECTOR = [\n  \"a[href]\",\n  \"button:not([disabled])\",\n  \"input:not([disabled])\",\n  \"select:not([disabled])\",\n  \"textarea:not([disabled])\",\n  \"[tabindex]:not([tabindex='-1'])\",\n].join(\",\");\n\nfunction subscribeToMobileQuery(callback: () => void) {\n  const query = window.matchMedia(MOBILE_QUERY);\n  query.addEventListener(\"change\", callback);\n  return () => query.removeEventListener(\"change\", callback);\n}\n\nfunction getMobileSnapshot() {\n  return window.matchMedia(MOBILE_QUERY).matches;\n}\n\nfunction getServerMobileSnapshot() {\n  return false;\n}\n\nfunction useIsMobile() {\n  return useSyncExternalStore(\n    subscribeToMobileQuery,\n    getMobileSnapshot,\n    getServerMobileSnapshot,\n  );\n}\n\ninterface AnimatedSidebarContextValue {\n  isMobile: boolean;\n  layoutId: string;\n  open: boolean;\n  openMobile: boolean;\n  reduce: boolean;\n  setOpen: (open: boolean) => void;\n  setOpenMobile: (open: boolean) => void;\n  state: SidebarState;\n  toggleSidebar: () => void;\n  triggerRef: React.RefObject<HTMLButtonElement | null>;\n}\n\nconst AnimatedSidebarContext =\n  createContext<AnimatedSidebarContextValue | null>(null);\n\ninterface AnimatedSidebarPanelContextValue {\n  collapsed: boolean;\n  collapsible: SidebarCollapsible;\n  side: SidebarSide;\n}\n\nconst AnimatedSidebarPanelContext =\n  createContext<AnimatedSidebarPanelContextValue | null>(null);\n\nexport function useAnimatedSidebar() {\n  const context = useContext(AnimatedSidebarContext);\n  if (!context) {\n    throw new Error(\n      \"useAnimatedSidebar must be used inside AnimatedSidebarProvider.\",\n    );\n  }\n  return context;\n}\n\nfunction useAnimatedSidebarPanel() {\n  const context = useContext(AnimatedSidebarPanelContext);\n  if (!context) {\n    throw new Error(\n      \"Animated Sidebar parts must be used inside AnimatedSidebar.\",\n    );\n  }\n  return context;\n}\n\ntype SidebarProviderStyle = CSSProperties & {\n  \"--sidebar-width\"?: string;\n  \"--sidebar-width-icon\"?: string;\n  \"--sidebar-width-mobile\"?: string;\n};\n\nexport interface AnimatedSidebarProviderProps\n  extends HTMLAttributes<HTMLDivElement> {\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  openMobile?: boolean;\n  defaultOpenMobile?: boolean;\n  onOpenMobileChange?: (open: boolean) => void;\n  style?: SidebarProviderStyle;\n}\n\nexport function AnimatedSidebarProvider({\n  children,\n  open,\n  defaultOpen = true,\n  onOpenChange,\n  openMobile,\n  defaultOpenMobile = false,\n  onOpenMobileChange,\n  className,\n  style,\n  ...props\n}: AnimatedSidebarProviderProps) {\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const [internalOpenMobile, setInternalOpenMobile] =\n    useState(defaultOpenMobile);\n  const isMobile = useIsMobile();\n  const reduce = useReducedMotion() ?? false;\n  const generatedId = useId();\n  const triggerRef = useRef<HTMLButtonElement>(null);\n  const desktopOpen = open ?? internalOpen;\n  const mobileOpen = openMobile ?? internalOpenMobile;\n\n  const setOpen = useCallback(\n    (nextOpen: boolean) => {\n      if (open === undefined) setInternalOpen(nextOpen);\n      onOpenChange?.(nextOpen);\n    },\n    [onOpenChange, open],\n  );\n\n  const setOpenMobile = useCallback(\n    (nextOpen: boolean) => {\n      if (openMobile === undefined) setInternalOpenMobile(nextOpen);\n      onOpenMobileChange?.(nextOpen);\n    },\n    [onOpenMobileChange, openMobile],\n  );\n\n  const toggleSidebar = useCallback(() => {\n    if (isMobile) setOpenMobile(!mobileOpen);\n    else setOpen(!desktopOpen);\n  }, [desktopOpen, isMobile, mobileOpen, setOpen, setOpenMobile]);\n\n  useEffect(() => {\n    const handleShortcut = (event: KeyboardEvent) => {\n      if (\n        event.key.toLowerCase() === SIDEBAR_KEYBOARD_SHORTCUT &&\n        (event.metaKey || event.ctrlKey)\n      ) {\n        event.preventDefault();\n        toggleSidebar();\n      }\n    };\n\n    window.addEventListener(\"keydown\", handleShortcut);\n    return () => window.removeEventListener(\"keydown\", handleShortcut);\n  }, [toggleSidebar]);\n\n  return (\n    <AnimatedSidebarContext.Provider\n      value={{\n        isMobile,\n        layoutId: `${generatedId}-active`,\n        open: desktopOpen,\n        openMobile: mobileOpen,\n        reduce,\n        setOpen,\n        setOpenMobile,\n        state: desktopOpen ? \"expanded\" : \"collapsed\",\n        toggleSidebar,\n        triggerRef,\n      }}\n    >\n      <div\n        {...props}\n        data-slot=\"sidebar-wrapper\"\n        data-state={desktopOpen ? \"expanded\" : \"collapsed\"}\n        style={{\n          \"--sidebar-width\": \"16rem\",\n          \"--sidebar-width-icon\": \"4.25rem\",\n          \"--sidebar-width-mobile\": \"18rem\",\n          ...style,\n        }}\n        className={cn(\n          \"group/sidebar-wrapper flex min-h-svh w-full min-w-0\",\n          className,\n        )}\n      >\n        {children}\n      </div>\n    </AnimatedSidebarContext.Provider>\n  );\n}\n\nfunction MobileSidebar({\n  ariaLabel,\n  children,\n  className,\n  side,\n}: {\n  ariaLabel: string;\n  children: ReactNode;\n  className?: string;\n  side: SidebarSide;\n}) {\n  const context = useAnimatedSidebar();\n  const panelRef = useRef<HTMLDivElement>(null);\n  const [mounted, setMounted] = useState(false);\n  // 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. See tests/fixed-overlay-edge-sampling.test.tsx.\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": "lib/utils.ts",
      "type": "registry:lib",
      "target": "@lib/utils.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"
    },
    {
      "path": "components/motion/button/base.tsx",
      "type": "registry:component",
      "target": "@components/motion/button/base.tsx",
      "content": "\"use client\";\n\nimport {\n  AnimatePresence,\n  type HTMLMotionProps,\n  motion,\n  useReducedMotion,\n} from \"motion/react\";\nimport {\n  forwardRef,\n  type PointerEvent,\n  type ReactNode,\n  useCallback,\n  useRef,\n  useState,\n} from \"react\";\nimport { EASE_OUT, SPRING_PRESS } from \"@/lib/ease\";\nimport { useHoverCapable } from \"@/lib/hooks/use-hover-capable\";\nimport { cn } from \"@/lib/utils\";\n\nexport type ButtonVariant = \"primary\" | \"secondary\" | \"ghost\" | \"outline\";\nexport type ButtonSize = \"sm\" | \"md\" | \"lg\" | \"icon\";\n\nexport interface ButtonProps extends Omit<\n  HTMLMotionProps<\"button\">,\n  \"children\"\n> {\n  variant?: ButtonVariant;\n  size?: ButtonSize;\n  pressScale?: number;\n  /** Spawn a Material-style ripple from the press point. Off by default. */\n  ripple?: boolean;\n  children?: ReactNode;\n}\n\nexport interface ButtonLinkProps extends Omit<\n  HTMLMotionProps<\"a\">,\n  \"children\"\n> {\n  variant?: ButtonVariant;\n  size?: ButtonSize;\n  pressScale?: number;\n  children?: ReactNode;\n}\n\ntype Ripple = { id: number; x: number; y: number; size: number };\n\nconst VARIANT_CLASS: Record<ButtonVariant, string> = {\n  primary: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n  secondary: \"border border-border bg-card text-foreground hover:border-border\",\n  ghost: \"text-muted-foreground hover:text-foreground hover:bg-muted/60\",\n  outline:\n    \"border border-border bg-transparent text-foreground hover:bg-muted/60\",\n};\n\nconst SIZE_CLASS: Record<ButtonSize, string> = {\n  sm: \"h-8 px-3 text-xs gap-1.5 rounded-full\",\n  md: \"h-10 px-5 text-sm gap-2 rounded-full\",\n  lg: \"h-12 px-6 text-base gap-2 rounded-full\",\n  icon: \"h-8 w-8 rounded-lg\",\n};\n\nexport const Button = forwardRef<HTMLButtonElement, ButtonProps>(\n  function Button(\n    {\n      variant = \"primary\",\n      size = \"md\",\n      pressScale = 0.93,\n      ripple = false,\n      className,\n      children,\n      onPointerDown,\n      ...rest\n    },\n    ref,\n  ) {\n    const reduce = useReducedMotion();\n    const canHover = useHoverCapable();\n    const [ripples, setRipples] = useState<Ripple[]>([]);\n    const nextId = useRef(0);\n\n    const handlePointerDown = useCallback(\n      (event: PointerEvent<HTMLButtonElement>) => {\n        if (ripple && !reduce) {\n          const rect = event.currentTarget.getBoundingClientRect();\n          const size = Math.max(rect.width, rect.height) * 2;\n          const id = nextId.current++;\n          setRipples((prev) => [\n            ...prev,\n            {\n              id,\n              x: event.clientX - rect.left,\n              y: event.clientY - rect.top,\n              size,\n            },\n          ]);\n        }\n        onPointerDown?.(event);\n      },\n      [ripple, reduce, onPointerDown],\n    );\n\n    return (\n      <motion.button\n        ref={ref}\n        type=\"button\"\n        whileTap={reduce ? undefined : { scale: pressScale }}\n        whileHover={reduce || !canHover ? undefined : { scale: 1.02 }}\n        transition={SPRING_PRESS}\n        onPointerDown={handlePointerDown}\n        className={cn(\n          \"inline-flex items-center justify-center font-medium select-none\",\n          \"transition-colors\",\n          \"disabled:pointer-events-none disabled:opacity-50\",\n          ripple && \"relative overflow-hidden\",\n          VARIANT_CLASS[variant],\n          SIZE_CLASS[size],\n          className,\n        )}\n        {...rest}\n      >\n        {ripple && !reduce ? (\n          <span className=\"pointer-events-none absolute inset-0 overflow-hidden rounded-[inherit]\">\n            <AnimatePresence>\n              {ripples.map((r) => (\n                <motion.span\n                  key={r.id}\n                  className=\"absolute rounded-full bg-current\"\n                  style={{\n                    left: r.x,\n                    top: r.y,\n                    width: r.size,\n                    height: r.size,\n                    x: \"-50%\",\n                    y: \"-50%\",\n                  }}\n                  initial={{ scale: 0.05, opacity: 0.3 }}\n                  animate={{ scale: 1, opacity: 0 }}\n                  exit={{ opacity: 0 }}\n                  transition={{ duration: 1.6, ease: EASE_OUT }}\n                  onAnimationComplete={() =>\n                    setRipples((prev) => prev.filter((x) => x.id !== r.id))\n                  }\n                />\n              ))}\n            </AnimatePresence>\n          </span>\n        ) : null}\n        {children}\n      </motion.button>\n    );\n  },\n);\n\nexport const ButtonLink = forwardRef<HTMLAnchorElement, ButtonLinkProps>(\n  function ButtonLink(\n    {\n      variant = \"primary\",\n      size = \"md\",\n      pressScale = 0.93,\n      className,\n      children,\n      ...rest\n    },\n    ref,\n  ) {\n    const reduce = useReducedMotion();\n    const canHover = useHoverCapable();\n\n    return (\n      <motion.a\n        ref={ref}\n        whileTap={reduce ? undefined : { scale: pressScale }}\n        whileHover={reduce || !canHover ? undefined : { scale: 1.02 }}\n        transition={SPRING_PRESS}\n        className={cn(\n          \"inline-flex items-center justify-center font-medium select-none\",\n          \"transition-colors\",\n          VARIANT_CLASS[variant],\n          SIZE_CLASS[size],\n          className,\n        )}\n        {...rest}\n      >\n        {children}\n      </motion.a>\n    );\n  },\n);\n"
    },
    {
      "path": "components/motion/popover-morph.tsx",
      "type": "registry:component",
      "target": "@components/motion/popover-morph.tsx",
      "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  /** Draw the surface shadow. Default true. */\n  shadow?: boolean;\n  /** Runs after the portalled surface is positioned and visible. */\n  onOpenAutoFocus?: (content: HTMLDivElement) => void;\n  /** Override the panel landmark. Menus pass \"menu\" instead of the dialog default. */\n  role?: \"dialog\" | \"menu\";\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  shadow = true,\n  onOpenAutoFocus,\n  role = \"dialog\",\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 preferredLeft = layout\n    ? align === \"end\"\n      ? layout.trigger.left + layout.trigger.width - layout.content.width\n      : layout.trigger.left\n    : 0;\n  // Larger compositions, such as calendars, use the roomier side if needed.\n  const gutter = 8;\n  const below = layout\n    ? Math.max(0, window.innerHeight - layout.trigger.top - layout.trigger.height - sideOffset - gutter)\n    : 0;\n  const above = layout ? Math.max(0, layout.trigger.top - sideOffset - gutter) : 0;\n  const requestedSpace = side === \"bottom\" ? below : above;\n  const otherSpace = side === \"bottom\" ? above : below;\n  const resolvedSide = layout && layout.content.height > requestedSpace && otherSpace > requestedSpace\n    ? side === \"bottom\" ? \"top\" : \"bottom\"\n    : side;\n  const availableHeight = resolvedSide === \"bottom\" ? below : above;\n  const left = layout\n    ? Math.max(gutter, Math.min(preferredLeft, window.innerWidth - layout.content.width - gutter))\n    : 0;\n  const top = layout\n    ? resolvedSide === \"bottom\"\n      ? layout.trigger.top + layout.trigger.height + sideOffset\n      : Math.max(gutter, 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(resolvedSide, align, radius, 92),\n          transition: MORPH_CLIP_TRANSITION,\n        },\n        show: {\n          clipPath: clipAt(resolvedSide, 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  const focusedOnOpen = useRef(false);\n  useEffect(() => {\n    if (!ready || !isPresent || focusedOnOpen.current || !ctx.contentRef.current) return;\n    focusedOnOpen.current = true;\n    onOpenAutoFocus?.(ctx.contentRef.current);\n  }, [ready, isPresent, ctx.contentRef, onOpenAutoFocus]);\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(resolvedSide, align),\n      }}\n      className={cn(\"fixed z-[9999]\", shadow && \"[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={role}\n        aria-labelledby={ctx.triggerId}\n        variants={clip}\n        style={{ borderRadius: radius, maxHeight: layout ? availableHeight : undefined, overflowY: \"auto\" }}\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": "registry:lib",
      "target": "@lib/ease.ts",
      "content": "// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n\n/** Dragged handles and fills (sliders) — critically damped `useSpring` config,\n * so the value follows the pointer butterily and never rebounds off an end. */\nexport const SPRING_GLIDE = {\n  stiffness: 700,\n  damping: 50,\n  mass: 0.5,\n} as const;\n"
    },
    {
      "path": "lib/hooks/use-hover-capable.ts",
      "type": "registry:hook",
      "target": "@lib/hooks/use-hover-capable.ts",
      "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\n/**\n * Returns true only on devices that have a true hover (mouse / trackpad).\n * Touch devices fire phantom `:hover` on tap that sticks until tap-elsewhere\n * — gate hover-only effects (scale lifts, magnetic pulls) behind this.\n */\nexport function useHoverCapable() {\n  const [canHover, setCanHover] = useState(false);\n\n  useEffect(() => {\n    if (typeof window === \"undefined\" || !window.matchMedia) return;\n    const mq = window.matchMedia(\"(hover: hover) and (pointer: fine)\");\n    const update = () => setCanHover(mq.matches);\n    update();\n    mq.addEventListener?.(\"change\", update);\n    return () => mq.removeEventListener?.(\"change\", update);\n  }, []);\n\n  return canHover;\n}\n"
    },
    {
      "path": "lib/hooks/use-on-open.ts",
      "type": "registry:hook",
      "target": "@lib/hooks/use-on-open.ts",
      "content": "\"use client\";\n\nimport { useState } from \"react\";\n\n/**\n * Runs `start` during the render in which `open` becomes true.\n *\n * Opening a list starts a fresh session — an empty query, the highlight back at\n * the top — and that is a resolution, not a side effect: a passive effect runs\n * after the commit, so the new session would carry the last one's state for a\n * window in which a key press can land.\n *\n * `start` may only set state belonging to the calling component. Anything that\n * reaches outside it — a consumer's callback, a DOM write, moving focus — is a\n * side effect, and React rejects a callback that sets another component's state\n * during this one's render. Put those in an effect keyed to `open`.\n */\nexport function useOnOpen(open: boolean, start: () => void) {\n  const [wasOpen, setWasOpen] = useState(open);\n  if (open !== wasOpen) {\n    setWasOpen(open);\n    if (open) start();\n  }\n}\n"
    },
    {
      "path": "components/motion/shared-layout-bg.tsx",
      "type": "registry:component",
      "target": "@components/motion/shared-layout-bg.tsx",
      "content": "\"use client\";\n\nimport {\n  AnimatePresence,\n  type HTMLMotionProps,\n  motion,\n  useReducedMotion,\n  type Variants,\n} from \"motion/react\";\nimport {\n  Children,\n  cloneElement,\n  forwardRef,\n  type HTMLAttributes,\n  isValidElement,\n  type MouseEvent,\n  type ReactElement,\n  type ReactNode,\n  type Ref,\n  useId,\n  useState,\n} from \"react\";\nimport { SPRING_LAYOUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface SharedLayoutBgProps\n  extends Omit<HTMLAttributes<HTMLElement>, \"children\"> {\n  children: ReactNode;\n  /** Semantic container used for the children. */\n  as?: \"div\" | \"ul\";\n  /** Tailwind class applied to the moving pill. Defaults to a subtle foreground tint. */\n  pillClassName?: string;\n  /** Horizontal inset of the pill relative to each row (px). Default 20. */\n  inset?: number;\n  /** Optional positioning override for the pill wrapper inside each item. */\n  pillContainerClassName?: string;\n}\n\nconst variants: Variants = {\n  initial: { opacity: 0, filter: \"blur(6px)\" },\n  animate: { opacity: 1, filter: \"blur(0px)\" },\n  exit: (isActive: boolean) =>\n    !isActive ? { opacity: 0, filter: \"blur(6px)\" } : {},\n};\n\nconst reducedVariants: Variants = {\n  initial: { opacity: 0 },\n  animate: { opacity: 1 },\n  exit: (isActive: boolean) => (!isActive ? { opacity: 0 } : {}),\n};\n\nexport const SharedLayoutBg = forwardRef<HTMLElement, SharedLayoutBgProps>(\n  function SharedLayoutBg(\n    {\n      children,\n      as = \"div\",\n      className,\n      onMouseLeave,\n      pillClassName,\n      pillContainerClassName,\n      inset = 20,\n      ...props\n    },\n    forwardedRef,\n  ) {\n  const [activeId, setActiveId] = useState<string | null>(null);\n  const uid = useId();\n  const reduce = useReducedMotion();\n\n    const renderedChildren = Children.toArray(children)\n      .filter(isValidElement)\n      .map((child, index) => {\n        const el = child as ReactElement<{\n          className?: string;\n          onMouseEnter?: () => void;\n          children?: ReactNode;\n        }>;\n        const childKey = el.key ? String(el.key) : `item-${index}`;\n        return cloneElement(\n          el,\n          {\n            key: childKey,\n            className: cn(\"relative\", el.props.className),\n            onMouseEnter: () => {\n              el.props.onMouseEnter?.();\n              setActiveId(childKey);\n            },\n          },\n          <>\n            <AnimatePresence custom={activeId !== null}>\n              {activeId !== null ? (\n                <motion.div\n                  variants={reduce ? reducedVariants : variants}\n                  initial=\"initial\"\n                  animate=\"animate\"\n                  exit=\"exit\"\n                  custom={activeId !== null}\n                  className={cn(\n                    \"pointer-events-none absolute inset-y-0\",\n                    pillContainerClassName,\n                  )}\n                  style={{ left: -inset, right: -inset }}\n                >\n                  {activeId === childKey ? (\n                    <motion.div\n                      layoutId={`shared-bg-${uid}`}\n                      transition={reduce ? { duration: 0 } : SPRING_LAYOUT}\n                      className={cn(\n                        \"pointer-events-none h-full w-full rounded-2xl bg-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"
    },
    {
      "path": "components/motion/popover-position.ts",
      "type": "registry:component",
      "target": "@components/motion/popover-position.ts",
      "content": "\"use client\";\n\nimport {\n  type MutableRefObject,\n  useCallback,\n  useLayoutEffect,\n  useState,\n} from \"react\";\n\nexport type PortalLayout = {\n  trigger: {\n    left: number;\n    top: number;\n    width: number;\n    height: number;\n  };\n  content: {\n    width: number;\n    height: number;\n  };\n};\n\nfunction sameLayout(a: PortalLayout | null, b: PortalLayout) {\n  return (\n    a?.trigger.left === b.trigger.left &&\n    a.trigger.top === b.trigger.top &&\n    a.trigger.width === b.trigger.width &&\n    a.trigger.height === b.trigger.height &&\n    a.content.width === b.content.width &&\n    a.content.height === b.content.height\n  );\n}\n\n/** Measures a trigger and portalled panel in viewport coordinates. */\nexport function usePopoverPortalPosition<\n  TriggerElement extends HTMLElement,\n  ContentElement extends HTMLElement,\n>(\n  triggerRef: MutableRefObject<TriggerElement | null>,\n  contentRef: MutableRefObject<ContentElement | null>,\n  active: boolean,\n) {\n  const [layout, setLayout] = useState<PortalLayout | null>(null);\n\n  const update = useCallback(() => {\n    const trigger = triggerRef.current;\n    const content = contentRef.current;\n    if (!trigger || !content) return;\n\n    const rect = trigger.getBoundingClientRect();\n    const next: PortalLayout = {\n      trigger: {\n        left: rect.left,\n        top: rect.top,\n        width: rect.width,\n        height: rect.height,\n      },\n      content: {\n        width: content.offsetWidth,\n        height: content.offsetHeight,\n      },\n    };\n    setLayout((current) => (sameLayout(current, next) ? current : next));\n  }, [contentRef, triggerRef]);\n\n  useLayoutEffect(() => {\n    update();\n    if (!active) return;\n\n    const trigger = triggerRef.current;\n    const content = contentRef.current;\n    const observer = new ResizeObserver(update);\n    if (trigger) observer.observe(trigger);\n    if (content) observer.observe(content);\n\n    window.addEventListener(\"scroll\", update, true);\n    window.addEventListener(\"resize\", update);\n    return () => {\n      observer.disconnect();\n      window.removeEventListener(\"scroll\", update, true);\n      window.removeEventListener(\"resize\", update);\n    };\n  }, [active, contentRef, triggerRef, update]);\n\n  return layout;\n}\n"
    }
  ]
}
