{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"dropdown-menu","type":"registry:component","title":"Dropdown Menu","description":"Composable animated dropdown menu: a corner-origin spring entrance with viewport collision flip, one shared focus surface that glides between rows for both pointer and keyboard, grouped labels, two-line items with icons and shortcuts, checkbox items, a hover/arrow-key submenu, and full roving-focus keyboard navigation.","author":"UI Lab","dependencies":["clsx","lucide-react","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/dropdown-menu.tsx","type":"registry:component","target":"@components/motion/dropdown-menu.tsx","content":"\"use client\";\n// ui-lab-ten.vercel.app/components/motion/dropdown-menu\n\nimport { Check, ChevronRight } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport {\n  createContext,\n  type KeyboardEvent as ReactKeyboardEvent,\n  type ReactNode,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { EASE_OUT, SPRING_LAYOUT, SPRING_PANEL } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\n// Submenus close on a short grace delay so the pointer can cut the corner\n// between the trigger row and the panel without the panel vanishing.\nconst SUB_CLOSE_GRACE_MS = 150;\n// Space (px) required below the trigger before the panel flips upward.\nconst FLIP_MARGIN = 16;\n\ntype Placement = \"bottom\" | \"top\";\ntype Align = \"start\" | \"center\" | \"end\";\n\ninterface MenuContextValue {\n  open: boolean;\n  setOpen: (open: boolean) => void;\n  /** Close the whole menu; optionally hand focus back to the trigger. */\n  close: (focusTrigger?: boolean) => void;\n  reduce: boolean;\n  triggerId: string;\n  menuId: string;\n}\n\nconst MenuContext = createContext<MenuContextValue | null>(null);\n\nfunction useMenuContext(component: string) {\n  const ctx = useContext(MenuContext);\n  if (!ctx) throw new Error(`${component} must be used within <DropdownMenu>`);\n  return ctx;\n}\n\n// Each panel (root content and every submenu) owns one gliding focus surface.\n// Hover and keyboard focus write the same `activeKey`, so there is a single\n// highlight that slides between rows instead of per-row backgrounds.\ninterface PanelContextValue {\n  surfaceId: string;\n  activeKey: string | null;\n  setActiveKey: (key: string | null) => void;\n}\n\nconst PanelContext = createContext<PanelContextValue | null>(null);\n\nfunction usePanelContext(component: string) {\n  const ctx = useContext(PanelContext);\n  if (!ctx)\n    throw new Error(`${component} must be used within a dropdown panel`);\n  return ctx;\n}\n\n/** Enabled menu items belonging to this panel only (submenu items excluded). */\nfunction panelItems(panel: HTMLElement) {\n  return Array.from(\n    panel.querySelectorAll<HTMLButtonElement>(\"[data-menu-item]\"),\n  ).filter(\n    (el) => el.closest(\"[data-menu-panel]\") === panel && !el.disabled,\n  );\n}\n\n// Shared roving-focus keyboard handling for the root panel and submenus.\n// Real focus moves between item buttons; Enter/Space stay native button\n// activation, so only navigation keys are handled here.\nfunction handlePanelKeys(event: ReactKeyboardEvent<HTMLDivElement>) {\n  const { key } = event;\n  if (![\"ArrowDown\", \"ArrowUp\", \"Home\", \"End\"].includes(key)) return;\n  const panel = event.currentTarget;\n  const items = panelItems(panel);\n  if (items.length === 0) return;\n  event.preventDefault();\n  event.stopPropagation();\n  const current = items.indexOf(document.activeElement as HTMLButtonElement);\n  let next = 0;\n  if (key === \"Home\") next = 0;\n  else if (key === \"End\") next = items.length - 1;\n  else if (key === \"ArrowDown\")\n    next = current < 0 ? 0 : (current + 1) % items.length;\n  else next = current < 0 ? items.length - 1 : (current - 1 + items.length) % items.length;\n  items[next]?.focus();\n}\n\nexport interface DropdownMenuProps {\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  className?: string;\n  children: ReactNode;\n}\n\nexport function DropdownMenu({\n  open: openProp,\n  defaultOpen = false,\n  onOpenChange,\n  className,\n  children,\n}: DropdownMenuProps) {\n  const reduce = useReducedMotion() ?? false;\n  const baseId = useId();\n  const rootRef = useRef<HTMLDivElement>(null);\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n\n  const controlled = openProp !== undefined;\n  const open = controlled ? openProp : internalOpen;\n\n  const setOpen = useCallback(\n    (next: boolean) => {\n      if (!controlled) setInternalOpen(next);\n      onOpenChange?.(next);\n    },\n    [controlled, onOpenChange],\n  );\n\n  const triggerId = `${baseId}-trigger`;\n\n  const close = useCallback(\n    (focusTrigger = false) => {\n      setOpen(false);\n      if (focusTrigger) document.getElementById(triggerId)?.focus();\n    },\n    [setOpen, triggerId],\n  );\n\n  // Outside pointer closes; Escape closes and restores trigger focus.\n  useEffect(() => {\n    if (!open) return;\n    const onKey = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") close(true);\n    };\n    const onPointer = (e: PointerEvent) => {\n      if (rootRef.current && !rootRef.current.contains(e.target as Node))\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, close]);\n\n  const ctx = useMemo<MenuContextValue>(\n    () => ({\n      open,\n      setOpen,\n      close,\n      reduce,\n      triggerId,\n      menuId: `${baseId}-menu`,\n    }),\n    [open, setOpen, close, reduce, triggerId, baseId],\n  );\n\n  return (\n    <MenuContext.Provider value={ctx}>\n      {/* h-fit defends against flex parents with default align-items: stretch —\n          a stretched root would anchor the top-full panel far below the trigger. */}\n      <div\n        ref={rootRef}\n        className={cn(\"relative inline-block h-fit\", className)}\n      >\n        {children}\n      </div>\n    </MenuContext.Provider>\n  );\n}\n\nexport interface DropdownMenuTriggerProps {\n  className?: string;\n  children: ReactNode;\n}\n\nexport function DropdownMenuTrigger({\n  className,\n  children,\n}: DropdownMenuTriggerProps) {\n  const ctx = useMenuContext(\"DropdownMenuTrigger\");\n  return (\n    <button\n      type=\"button\"\n      id={ctx.triggerId}\n      aria-haspopup=\"menu\"\n      aria-expanded={ctx.open}\n      aria-controls={ctx.menuId}\n      onClick={() => ctx.setOpen(!ctx.open)}\n      className={cn(\n        \"flex items-center gap-2 rounded-xl border border-border bg-background px-3 py-2 text-sm text-foreground outline-none transition-colors\",\n        \"hover:border-(--color-border-strong) focus-visible:ring-2 focus-visible:ring-foreground/20\",\n        className,\n      )}\n    >\n      {children}\n    </button>\n  );\n}\n\nexport interface DropdownMenuContentProps {\n  align?: Align;\n  className?: string;\n  children: ReactNode;\n}\n\nexport function DropdownMenuContent({\n  align = \"start\",\n  className,\n  children,\n}: DropdownMenuContentProps) {\n  const ctx = useMenuContext(\"DropdownMenuContent\");\n  const panelRef = useRef<HTMLDivElement>(null);\n  const surfaceId = useId();\n  const [activeKey, setActiveKey] = useState<string | null>(null);\n  const [placement, setPlacement] = useState<Placement>(\"bottom\");\n  const open = ctx.open;\n\n  // On open, flip upward when there isn't room below and there's more above\n  // (same viewport check as Select).\n  useLayoutEffect(() => {\n    if (!open) return;\n    const trigger = document.getElementById(ctx.triggerId);\n    const panel = panelRef.current;\n    if (!trigger || !panel) return;\n    const rect = trigger.getBoundingClientRect();\n    const h = panel.offsetHeight;\n    const below = window.innerHeight - rect.bottom;\n    setPlacement(below < h + FLIP_MARGIN && rect.top > below ? \"top\" : \"bottom\");\n  }, [open, ctx.triggerId]);\n\n  // Focus the panel on open so navigation keys work immediately; clear the\n  // gliding surface whenever the menu closes. Skip the focus grab when nothing\n  // on the page holds focus yet (e.g. a `defaultOpen` menu on first paint) so\n  // an open-by-default demo never steals focus on load.\n  useEffect(() => {\n    if (open) {\n      const raf = requestAnimationFrame(() => {\n        if (document.activeElement === document.body) return;\n        panelRef.current?.focus();\n      });\n      return () => cancelAnimationFrame(raf);\n    }\n    setActiveKey(null);\n  }, [open]);\n\n  const panelCtx = useMemo<PanelContextValue>(\n    () => ({ surfaceId, activeKey, setActiveKey }),\n    [surfaceId, activeKey],\n  );\n\n  const isTop = placement === \"top\";\n  const originY = isTop ? \"bottom\" : \"top\";\n  const originX =\n    align === \"center\" ? \"center\" : align === \"end\" ? \"right\" : \"left\";\n\n  return (\n    <PanelContext.Provider value={panelCtx}>\n      <motion.div\n        ref={panelRef}\n        id={ctx.menuId}\n        role=\"menu\"\n        aria-labelledby={ctx.triggerId}\n        tabIndex={-1}\n        inert={!open}\n        data-menu-panel\n        onKeyDown={handlePanelKeys}\n        onMouseLeave={() => {\n          // Keep the highlight when focus is inside (keyboard user just\n          // happens to move the pointer away).\n          const panel = panelRef.current;\n          if (panel?.contains(document.activeElement)) return;\n          setActiveKey(null);\n        }}\n        initial={false}\n        animate={{\n          opacity: open ? 1 : 0,\n          scale: ctx.reduce ? 1 : open ? 1 : 0.94,\n          y: ctx.reduce ? 0 : open ? 0 : isTop ? 4 : -4,\n          x: align === \"center\" ? \"-50%\" : 0,\n        }}\n        transition={\n          open\n            ? ctx.reduce\n              ? { duration: 0.15, ease: EASE_OUT }\n              : SPRING_PANEL\n            : { duration: 0.12, ease: EASE_OUT }\n        }\n        style={{\n          transformOrigin: `${originY} ${originX}`,\n          pointerEvents: open ? \"auto\" : \"none\",\n        }}\n        className={cn(\n          \"absolute z-50 min-w-56 rounded-xl border border-border bg-background p-1 shadow-lg outline-none\",\n          isTop ? \"bottom-full mb-1.5\" : \"top-full mt-1.5\",\n          align === \"start\" && \"left-0\",\n          align === \"center\" && \"left-1/2\",\n          align === \"end\" && \"right-0\",\n          className,\n        )}\n      >\n        {children}\n      </motion.div>\n    </PanelContext.Provider>\n  );\n}\n\nexport interface DropdownMenuLabelProps {\n  className?: string;\n  children: ReactNode;\n}\n\nexport function DropdownMenuLabel({\n  className,\n  children,\n}: DropdownMenuLabelProps) {\n  return (\n    <div\n      className={cn(\n        \"px-2.5 pt-1.5 pb-1 text-xs font-medium text-muted-foreground/80\",\n        className,\n      )}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport interface DropdownMenuSeparatorProps {\n  className?: string;\n}\n\nexport function DropdownMenuSeparator({\n  className,\n}: DropdownMenuSeparatorProps) {\n  return (\n    <hr className={cn(\"-mx-1 my-1 h-px border-0 bg-border\", className)} />\n  );\n}\n\n// Row shell shared by items, checkbox items and submenu triggers: a relative\n// wrapper hosting the gliding focus surface behind the real button.\ninterface ItemShellProps {\n  active: boolean;\n  children: ReactNode;\n}\n\nfunction ItemShell({ active, children }: ItemShellProps) {\n  const panel = usePanelContext(\"DropdownMenu item\");\n  const reduce = useReducedMotion() ?? false;\n  return (\n    <div className=\"relative\">\n      <AnimatePresence>\n        {active ? (\n          <motion.div\n            layoutId={panel.surfaceId}\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 1, transition: { duration: 0.15 } }}\n            exit={{ opacity: 0, transition: { duration: 0.1 } }}\n            transition={reduce ? { duration: 0 } : SPRING_LAYOUT}\n            className=\"pointer-events-none absolute inset-0 rounded-lg bg-muted\"\n          />\n        ) : null}\n      </AnimatePresence>\n      {children}\n    </div>\n  );\n}\n\n// Hover and focus funnel into the panel's single activeKey.\nfunction useItemActivation(itemKey: string) {\n  const panel = usePanelContext(\"DropdownMenu item\");\n  return {\n    active: panel.activeKey === itemKey,\n    handlers: {\n      onMouseEnter: () => panel.setActiveKey(itemKey),\n      onFocus: () => panel.setActiveKey(itemKey),\n    },\n  };\n}\n\nconst ITEM_BUTTON_CLASSES =\n  \"relative z-10 flex w-full items-center gap-2.5 rounded-lg px-2.5 py-2 text-left text-sm outline-none transition-colors disabled:pointer-events-none disabled:opacity-50\";\n\nexport interface DropdownMenuItemProps {\n  icon?: ReactNode;\n  description?: string;\n  shortcut?: string;\n  disabled?: boolean;\n  destructive?: boolean;\n  onSelect?: () => void;\n  className?: string;\n  children: ReactNode;\n}\n\nexport function DropdownMenuItem({\n  icon,\n  description,\n  shortcut,\n  disabled = false,\n  destructive = false,\n  onSelect,\n  className,\n  children,\n}: DropdownMenuItemProps) {\n  const menu = useMenuContext(\"DropdownMenuItem\");\n  const itemKey = useId();\n  const { active, handlers } = useItemActivation(itemKey);\n  return (\n    <ItemShell active={active}>\n      <button\n        type=\"button\"\n        role=\"menuitem\"\n        data-menu-item\n        disabled={disabled}\n        {...handlers}\n        onClick={() => {\n          onSelect?.();\n          menu.close(true);\n        }}\n        className={cn(\n          ITEM_BUTTON_CLASSES,\n          destructive\n            ? \"text-destructive\"\n            : active\n              ? \"text-foreground\"\n              : \"text-muted-foreground\",\n          className,\n        )}\n      >\n        {icon ? (\n          <span\n            aria-hidden=\"true\"\n            className={cn(\n              \"shrink-0 [&>svg]:h-4 [&>svg]:w-4\",\n              destructive ? \"text-destructive\" : \"text-muted-foreground\",\n            )}\n          >\n            {icon}\n          </span>\n        ) : null}\n        <span className=\"min-w-0 flex-1\">\n          <span className=\"block truncate\">{children}</span>\n          {description ? (\n            <span className=\"block truncate text-xs text-muted-foreground\">\n              {description}\n            </span>\n          ) : null}\n        </span>\n        {shortcut ? (\n          <kbd className=\"shrink-0 font-mono text-[11px] text-muted-foreground/70\">\n            {shortcut}\n          </kbd>\n        ) : null}\n      </button>\n    </ItemShell>\n  );\n}\n\nexport interface DropdownMenuCheckboxItemProps {\n  checked: boolean;\n  onCheckedChange: (checked: boolean) => void;\n  disabled?: boolean;\n  className?: string;\n  children: ReactNode;\n}\n\nexport function DropdownMenuCheckboxItem({\n  checked,\n  onCheckedChange,\n  disabled = false,\n  className,\n  children,\n}: DropdownMenuCheckboxItemProps) {\n  const reduce = useReducedMotion() ?? false;\n  const itemKey = useId();\n  const { active, handlers } = useItemActivation(itemKey);\n  return (\n    <ItemShell active={active}>\n      <button\n        type=\"button\"\n        role=\"menuitemcheckbox\"\n        aria-checked={checked}\n        data-menu-item\n        disabled={disabled}\n        {...handlers}\n        onClick={() => onCheckedChange(!checked)}\n        className={cn(\n          ITEM_BUTTON_CLASSES,\n          active ? \"text-foreground\" : \"text-muted-foreground\",\n          className,\n        )}\n      >\n        <span\n          aria-hidden=\"true\"\n          className=\"flex h-4 w-4 shrink-0 items-center justify-center\"\n        >\n          <AnimatePresence initial={false}>\n            {checked ? (\n              <motion.span\n                initial={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.5 }}\n                animate={{ opacity: 1, scale: 1 }}\n                exit={\n                  reduce\n                    ? { opacity: 0, transition: { duration: 0.1 } }\n                    : {\n                        opacity: 0,\n                        scale: 0.5,\n                        transition: { duration: 0.1, ease: EASE_OUT },\n                      }\n                }\n                transition={{ duration: 0.15, ease: EASE_OUT }}\n              >\n                <Check className=\"h-3.5 w-3.5\" />\n              </motion.span>\n            ) : null}\n          </AnimatePresence>\n        </span>\n        <span className=\"min-w-0 flex-1 truncate\">{children}</span>\n      </button>\n    </ItemShell>\n  );\n}\n\ninterface SubContextValue {\n  open: boolean;\n  openSub: () => void;\n  scheduleClose: () => void;\n  closeNow: (focusTrigger?: boolean) => void;\n  subTriggerId: string;\n  subMenuId: string;\n}\n\nconst SubContext = createContext<SubContextValue | null>(null);\n\nfunction useSubContext(component: string) {\n  const ctx = useContext(SubContext);\n  if (!ctx)\n    throw new Error(`${component} must be used within <DropdownMenuSub>`);\n  return ctx;\n}\n\nexport interface DropdownMenuSubProps {\n  className?: string;\n  children: ReactNode;\n}\n\nexport function DropdownMenuSub({ className, children }: DropdownMenuSubProps) {\n  const menu = useMenuContext(\"DropdownMenuSub\");\n  const baseId = useId();\n  const [open, setOpen] = useState(false);\n  const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  const clearTimer = useCallback(() => {\n    if (closeTimer.current) {\n      clearTimeout(closeTimer.current);\n      closeTimer.current = null;\n    }\n  }, []);\n\n  const openSub = useCallback(() => {\n    clearTimer();\n    setOpen(true);\n  }, [clearTimer]);\n\n  const subTriggerId = `${baseId}-subtrigger`;\n\n  const closeNow = useCallback(\n    (focusTrigger = false) => {\n      clearTimer();\n      setOpen(false);\n      if (focusTrigger) document.getElementById(subTriggerId)?.focus();\n    },\n    [clearTimer, subTriggerId],\n  );\n\n  const scheduleClose = useCallback(() => {\n    clearTimer();\n    closeTimer.current = setTimeout(() => setOpen(false), SUB_CLOSE_GRACE_MS);\n  }, [clearTimer]);\n\n  // Collapse with the root menu and never leak the grace timer.\n  useEffect(() => {\n    if (!menu.open) setOpen(false);\n  }, [menu.open]);\n  useEffect(() => clearTimer, [clearTimer]);\n\n  const ctx = useMemo<SubContextValue>(\n    () => ({\n      open,\n      openSub,\n      scheduleClose,\n      closeNow,\n      subTriggerId,\n      subMenuId: `${baseId}-submenu`,\n    }),\n    [open, openSub, scheduleClose, closeNow, subTriggerId, baseId],\n  );\n\n  return (\n    <SubContext.Provider value={ctx}>\n      {/* motion.div (not a static div) hosts the hover-intent handlers — same\n          pattern as SharedLayoutBg's container. */}\n      <motion.div\n        onMouseEnter={openSub}\n        onMouseLeave={scheduleClose}\n        className={cn(\"relative\", className)}\n      >\n        {children}\n      </motion.div>\n    </SubContext.Provider>\n  );\n}\n\nexport interface DropdownMenuSubTriggerProps {\n  icon?: ReactNode;\n  className?: string;\n  children: ReactNode;\n}\n\nexport function DropdownMenuSubTrigger({\n  icon,\n  className,\n  children,\n}: DropdownMenuSubTriggerProps) {\n  const sub = useSubContext(\"DropdownMenuSubTrigger\");\n  const itemKey = useId();\n  const { active, handlers } = useItemActivation(itemKey);\n  return (\n    <ItemShell active={active}>\n      <button\n        type=\"button\"\n        role=\"menuitem\"\n        id={sub.subTriggerId}\n        aria-haspopup=\"menu\"\n        aria-expanded={sub.open}\n        aria-controls={sub.subMenuId}\n        data-menu-item\n        {...handlers}\n        onClick={() => (sub.open ? sub.closeNow() : sub.openSub())}\n        onKeyDown={(e) => {\n          if (e.key !== \"ArrowRight\") return;\n          e.preventDefault();\n          e.stopPropagation();\n          sub.openSub();\n          // Focus lands on the submenu's first item once it has mounted.\n          requestAnimationFrame(() => {\n            const panel = document.getElementById(sub.subMenuId);\n            if (panel) panelItems(panel)[0]?.focus();\n          });\n        }}\n        className={cn(\n          ITEM_BUTTON_CLASSES,\n          active ? \"text-foreground\" : \"text-muted-foreground\",\n          className,\n        )}\n      >\n        {icon ? (\n          <span\n            aria-hidden=\"true\"\n            className=\"shrink-0 text-muted-foreground [&>svg]:h-4 [&>svg]:w-4\"\n          >\n            {icon}\n          </span>\n        ) : null}\n        <span className=\"min-w-0 flex-1 truncate\">{children}</span>\n        <ChevronRight\n          aria-hidden=\"true\"\n          className=\"h-3.5 w-3.5 shrink-0 text-muted-foreground/70\"\n        />\n      </button>\n    </ItemShell>\n  );\n}\n\nexport interface DropdownMenuSubContentProps {\n  className?: string;\n  children: ReactNode;\n}\n\nexport function DropdownMenuSubContent({\n  className,\n  children,\n}: DropdownMenuSubContentProps) {\n  const menu = useMenuContext(\"DropdownMenuSubContent\");\n  const sub = useSubContext(\"DropdownMenuSubContent\");\n  const panelRef = useRef<HTMLDivElement>(null);\n  const surfaceId = useId();\n  const [activeKey, setActiveKey] = useState<string | null>(null);\n  // Which side of the parent panel the submenu opens toward; flips to the\n  // left when the right edge would leave the viewport.\n  const [side, setSide] = useState<\"right\" | \"left\">(\"right\");\n\n  useLayoutEffect(() => {\n    if (!sub.open) return;\n    const panel = panelRef.current;\n    if (!panel) return;\n    const rect = panel.getBoundingClientRect();\n    if (side === \"right\" && rect.right > window.innerWidth - 8)\n      setSide(\"left\");\n  }, [sub.open, side]);\n\n  useEffect(() => {\n    if (!sub.open) setActiveKey(null);\n  }, [sub.open]);\n\n  const panelCtx = useMemo<PanelContextValue>(\n    () => ({ surfaceId, activeKey, setActiveKey }),\n    [surfaceId, activeKey],\n  );\n\n  return (\n    <PanelContext.Provider value={panelCtx}>\n      <AnimatePresence>\n        {sub.open ? (\n          <motion.div\n            ref={panelRef}\n            id={sub.subMenuId}\n            role=\"menu\"\n            aria-labelledby={sub.subTriggerId}\n            tabIndex={-1}\n            data-menu-panel\n            onKeyDown={(e) => {\n              if (e.key === \"ArrowLeft\") {\n                e.preventDefault();\n                e.stopPropagation();\n                sub.closeNow(true);\n                return;\n              }\n              handlePanelKeys(e);\n            }}\n            onMouseLeave={() => {\n              const panel = panelRef.current;\n              if (panel?.contains(document.activeElement)) return;\n              setActiveKey(null);\n            }}\n            initial={\n              menu.reduce\n                ? { opacity: 0 }\n                : { opacity: 0, scale: 0.96, x: side === \"right\" ? -4 : 4 }\n            }\n            animate={\n              menu.reduce\n                ? { opacity: 1 }\n                : { opacity: 1, scale: 1, x: 0 }\n            }\n            exit={{ opacity: 0, transition: { duration: 0.1, ease: EASE_OUT } }}\n            transition={menu.reduce ? { duration: 0.15 } : SPRING_PANEL}\n            style={{\n              transformOrigin: side === \"right\" ? \"top left\" : \"top right\",\n            }}\n            className={cn(\n              \"absolute -top-1 z-50 min-w-44 rounded-xl border border-border bg-background p-1 shadow-lg outline-none\",\n              side === \"right\" ? \"left-full ml-1\" : \"right-full mr-1\",\n              className,\n            )}\n          >\n            {children}\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n    </PanelContext.Provider>\n  );\n}\n"},{"path":"lib/ease.ts","type":"registry:lib","target":"@lib/ease.ts","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}