{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dropdown-menu",
  "type": "registry:ui",
  "title": "Dropdown Menu",
  "description": "Dropdown menu built on Base UI with seam overlay-depth entrance.",
  "dependencies": [
    "@base-ui/react",
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "https://seamui.dev/r/utils.json",
    "https://seamui.dev/r/motion.json",
    "https://seamui.dev/r/haptics.json"
  ],
  "files": [
    {
      "path": "registry/seam/ui/dropdown-menu.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Menu as BaseMenu } from \"@base-ui/react/menu\"\nimport { motion } from \"motion/react\"\nimport { Check, ChevronLeft, ChevronRight, Circle } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n  condense,\n  drill,\n  fades,\n  reduced,\n  springs,\n  useReducedMotion,\n} from \"@/lib/motion\"\nimport { useHaptics } from \"@/lib/haptics\"\n\n// Shared item shape so Item / CheckboxItem / RadioItem / SubTrigger stay in sync.\nconst menuItemClass =\n  \"relative flex cursor-default select-none items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0\"\n\n/* ────────────────────────────────────────────────────────────────────────────\n * Nested menus drill *into* the popup instead of flying out beside it.\n *\n * A submenu doesn't open a second surface anchored to its trigger; it replaces\n * the level below it inside the one popup, which springs to the new level's\n * size. Depth is unlimited and costs no horizontal room — the reason to do it\n * this way — and it behaves the same on a phone as on a wide desktop.\n *\n * The whole thing is one piece of state: `path`, the list of submenus you've\n * opened. The panel at `depth === path.length` is the one on screen. Levels\n * you've drilled past stay mounted and hidden (see `useParked` — it's what\n * keeps their uncontrolled state alive); levels off the open path aren't\n * mounted at all, exactly like a closed flyout submenu.\n *\n * **Only the visible level's items are ever rendered**, and that is\n * load-bearing, not cosmetic. Base UI's Menu is a composite: it registers items\n * by DOM node and roves focus across them, skipping any that `checkVisibility()`\n * says aren't rendered. So a level that lingered *visibly* to animate out would\n * be a level whose items still answer to the arrow keys — which is why the\n * outgoing level goes the instant the path changes, and the incoming level\n * carries the transition on its own (see `drill` in @/lib/motion).\n * ──────────────────────────────────────────────────────────────────────────── */\n\ntype DropdownLevel = {\n  id: string\n  /** What the level's back row is labelled with — the sub-trigger's content. */\n  label: React.ReactNode\n}\n\ntype DropdownNav = {\n  path: DropdownLevel[]\n  /** 1 drilling in, -1 stepping back — which way the incoming level slides. */\n  direction: 1 | -1\n  /** False until the first drill of this open, so the root level never slides\n   *  in behind the popup's own entrance. */\n  navigated: boolean\n  /** After stepping back, the sub whose trigger focus should return to. */\n  returnTo?: string\n}\n\nconst AT_ROOT: DropdownNav = { path: [], direction: 1, navigated: false }\n\nconst DropdownNavContext = React.createContext<\n  | (DropdownNav & {\n      push: (level: DropdownLevel) => void\n      pop: () => void\n    })\n  | null\n>(null)\n\nfunction useDropdownNav() {\n  const nav = React.useContext(DropdownNavContext)\n  if (!nav) {\n    throw new Error(\n      \"seamui: dropdown menu parts must be used inside <DropdownMenu>\"\n    )\n  }\n  return nav\n}\n\n/** The level a part is rendered into, and whether that level is the visible one. */\nconst DropdownPanelContext = React.createContext<{\n  depth: number\n  active: boolean\n}>({ depth: 0, active: true })\n\n/** How the viewport finds the level it should be sized to. */\nconst ACTIVE_PANEL = '[data-slot=\"dropdown-menu-panel\"][data-active]'\n\n/**\n * Parts on a level you've drilled past render hidden rather than not at all.\n *\n * Unmounting them would be simpler, but it throws away any uncontrolled state\n * they hold: a `defaultChecked` checkbox or a `defaultValue` radio group would\n * silently reset every time you drilled past its level and came back. A flyout\n * submenu never did that — the level underneath stayed mounted — so neither\n * should this.\n *\n * Hiding keeps the state and still keeps the parts out of the keyboard's way:\n * Base UI's list navigation runs `checkVisibility()` over the items it has\n * registered and skips the ones that aren't rendered. Comes last in `cn` so it\n * wins the display conflict against `flex` (and anything a caller passed).\n *\n * Levels *not* on the open path stay unmounted — that's a closed submenu, and\n * it dropped its uncontrolled state before this change too.\n */\nfunction useParked(): string | undefined {\n  const { active } = React.useContext(DropdownPanelContext)\n  return active ? undefined : \"hidden\"\n}\n\nconst DropdownSubContext = React.createContext<{ id: string } | null>(null)\n\nfunction DropdownMenu({\n  onOpenChange,\n  onOpenChangeComplete,\n  ...props\n}: React.ComponentProps<typeof BaseMenu.Root>) {\n  const [nav, setNav] = React.useState<DropdownNav>(AT_ROOT)\n\n  // Base UI holds onto its open-change handler, so read the path off a ref\n  // rather than the closure to decide what Escape means.\n  const navRef = React.useRef(nav)\n  navRef.current = nav\n\n  const push = React.useCallback((level: DropdownLevel) => {\n    setNav((prev) => ({\n      path: [...prev.path, level],\n      direction: 1,\n      navigated: true,\n    }))\n  }, [])\n\n  const pop = React.useCallback(() => {\n    setNav((prev) => ({\n      path: prev.path.slice(0, -1),\n      direction: -1,\n      navigated: true,\n      returnTo: prev.path.at(-1)?.id,\n    }))\n  }, [])\n\n  const value = React.useMemo(() => ({ ...nav, push, pop }), [nav, push, pop])\n\n  return (\n    <DropdownNavContext.Provider value={value}>\n      <BaseMenu.Root\n        onOpenChange={(open, details) => {\n          // Escape inside a nested level steps back one level instead of\n          // dismissing everything — what a flyout submenu would have done.\n          if (\n            !open &&\n            details.reason === \"escape-key\" &&\n            navRef.current.path.length > 0\n          ) {\n            details.cancel()\n            pop()\n            return\n          }\n          onOpenChange?.(open, details)\n        }}\n        onOpenChangeComplete={(open) => {\n          // Back to the root level only once the popup has finished leaving, so\n          // reopening never flashes the level the user drilled into.\n          if (!open) setNav(AT_ROOT)\n          onOpenChangeComplete?.(open)\n        }}\n        {...props}\n      />\n    </DropdownNavContext.Provider>\n  )\n}\n\nfunction DropdownMenuTrigger(\n  props: React.ComponentProps<typeof BaseMenu.Trigger>\n) {\n  return <BaseMenu.Trigger data-slot=\"dropdown-menu-trigger\" {...props} />\n}\n\n/**\n * The clipping box every level is drawn into. Springs between the size of the\n * level you left and the size of the one you entered, so the popup grows and\n * shrinks around the content instead of jumping.\n */\nfunction DropdownMenuViewport({ children }: { children?: React.ReactNode }) {\n  const { path, navigated, returnTo } = useDropdownNav()\n  const reduceMotion = useReducedMotion() ?? false\n  const viewportRef = React.useRef<HTMLDivElement | null>(null)\n  const [size, setSize] = React.useState<{\n    width: number\n    height: number\n  } | null>(null)\n\n  // The visible level is found in the DOM rather than handed over by a ref.\n  // The root level's element never unmounts — it only stops being active — so a\n  // ref on it isn't re-attached when you step back into it, and the viewport\n  // would keep the size of the level you just left. `data-active` is always\n  // right by the time layout effects run.\n  const measure = React.useCallback(() => {\n    const viewport = viewportRef.current\n    const panel = viewport?.querySelector<HTMLElement>(ACTIVE_PANEL)\n    if (!viewport || !panel) return\n    // Read the level at its *natural* size. The panel stretches to the viewport\n    // (`min-w-full`), so measuring while the viewport still holds the previous\n    // level's width would floor every level at the widest one before it —\n    // releasing the inline size is what lets a level get narrower. The restore\n    // happens in the same layout pass, so nothing paints at the released size.\n    const { width, height } = viewport.style\n    viewport.style.width = \"auto\"\n    viewport.style.height = \"auto\"\n    const next = { width: panel.offsetWidth, height: panel.offsetHeight }\n    viewport.style.width = width\n    viewport.style.height = height\n    setSize((prev) =>\n      prev && prev.width === next.width && prev.height === next.height\n        ? prev\n        : next\n    )\n  }, [])\n\n  // Re-measure whenever the level changes, and keep up with content that\n  // changes *within* a level too — a checkbox row gaining an indicator, an\n  // async label landing.\n  React.useLayoutEffect(() => {\n    measure()\n    const panel = viewportRef.current?.querySelector<HTMLElement>(ACTIVE_PANEL)\n    if (!panel || typeof ResizeObserver === \"undefined\") return\n    const observer = new ResizeObserver(measure)\n    observer.observe(panel)\n    return () => observer.disconnect()\n  }, [measure, path])\n\n  // Move focus onto the level a drill lands on. Base UI does this itself when\n  // items *mount*, but levels here are hidden rather than unmounted, so nothing\n  // moves and focus would sit on the popup — a keyboard user would drill in and\n  // see no highlight at all. Skipped before the first drill, where Base UI's own\n  // open focus is correct.\n  //\n  // Stepping back returns to the trigger you left through, like a flyout\n  // submenu closing. Drilling in lands on the back row: it is the level's first\n  // row, and it announces which level you just entered.\n  React.useEffect(() => {\n    if (!navigated) return\n    const panel = viewportRef.current?.querySelector<HTMLElement>(ACTIVE_PANEL)\n    if (!panel) return\n    const restored = returnTo\n      ? panel.querySelector<HTMLElement>(\n          `[data-sub-id=\"${CSS.escape(returnTo)}\"]`\n        )\n      : null\n    ;(\n      restored ?? panel.querySelector<HTMLElement>('[role^=\"menuitem\"]')\n    )?.focus()\n  }, [navigated, path, returnTo])\n\n  return (\n    <motion.div\n      ref={viewportRef}\n      data-slot=\"dropdown-menu-viewport\"\n      // The popup is the `role=\"menu\"`; this and the panels are layout only, so\n      // they step out of the a11y tree and leave the items as its children.\n      role=\"presentation\"\n      className=\"relative min-w-full overflow-hidden\"\n      // No entrance of its own — the popup's `condense.surface` covers the open,\n      // and the first measured size is applied without animating.\n      initial={false}\n      animate={size ?? {}}\n      transition={reduceMotion ? reduced.instant : springs.snappy}\n    >\n      {children}\n    </motion.div>\n  )\n}\n\n/**\n * One level of the menu. A level you've drilled *past* stays mounted as a\n * `display: contents` wrapper — it draws nothing and lays nothing out, but it\n * keeps the branch holding the visible level in place.\n *\n * That the wrapper never changes element type is deliberate. Swapping it for a\n * fragment when the level goes off screen remounts everything under it, and a\n * remounted `DropdownMenuSub` gets a fresh `useId` — the level you just opened\n * would stop recognising its own entry in `path` and vanish. Levels move\n * between visible and parked; they don't come and go.\n *\n * So the entrance is expressed as a target rather than a mount: parked levels\n * sit at `drill.enter(-1)` (off to the left, where you left them) and animate to\n * `drill.settle` when they come back. Levels *deeper* than the current one\n * aren't mounted at all, so those do enter on mount, from the right.\n */\nfunction DropdownMenuPanel({\n  depth,\n  className,\n  children,\n}: {\n  depth: number\n  className?: string\n  children?: React.ReactNode\n}) {\n  const { path, direction, navigated } = useDropdownNav()\n  const reduceMotion = useReducedMotion() ?? false\n  const active = depth === path.length\n\n  const value = React.useMemo(() => ({ depth, active }), [depth, active])\n\n  return (\n    <DropdownPanelContext.Provider value={value}>\n      <motion.div\n        data-slot=\"dropdown-menu-panel\"\n        data-active={active || undefined}\n        role=\"presentation\"\n        // `w-max` so the level keeps its natural width while the viewport\n        // springs; `min-w-full` so it still fills a wider popup.\n        className={active ? cn(\"w-max min-w-full\", className) : \"contents\"}\n        initial={\n          navigated\n            ? reduceMotion\n              ? reduced.fadeIn.initial\n              : drill.enter(direction)\n            : false\n        }\n        animate={\n          active\n            ? reduceMotion\n              ? reduced.fadeIn.animate\n              : drill.settle\n            : reduceMotion\n              ? reduced.fadeIn.initial\n              : drill.enter(-1)\n        }\n        // Parking is instant: a level has to be fully at its offset before it\n        // can come back, or a quick out-and-back enters from half a slide.\n        transition={\n          active\n            ? reduceMotion\n              ? fades.fast\n              : springs.snappy\n            : reduced.instant\n        }\n      >\n        {children}\n      </motion.div>\n    </DropdownPanelContext.Provider>\n  )\n}\n\nfunction DropdownMenuContent({\n  className,\n  sideOffset = 6,\n  align = \"start\",\n  onKeyDown,\n  children,\n  ...props\n}: React.ComponentProps<typeof BaseMenu.Popup> & {\n  sideOffset?: number\n  align?: \"start\" | \"center\" | \"end\"\n}) {\n  const { path, pop } = useDropdownNav()\n\n  return (\n    <BaseMenu.Portal>\n      <BaseMenu.Positioner sideOffset={sideOffset} align={align}>\n        <BaseMenu.Popup\n          data-slot=\"dropdown-menu-content\"\n          className={cn(\n            \"bg-popover text-popover-foreground z-50 w-max min-w-40 rounded-lg squircle border p-1 shadow-overlay outline-none\",\n            condense.surface,\n            className\n          )}\n          onKeyDown={(event) => {\n            onKeyDown?.(event)\n            if (event.defaultPrevented) return\n            // ArrowLeft steps back out of a level, mirroring how it closes a\n            // flyout submenu.\n            if (event.key === \"ArrowLeft\" && path.length > 0) {\n              event.preventDefault()\n              pop()\n            }\n          }}\n          {...props}\n        >\n          <DropdownMenuViewport>\n            <DropdownMenuPanel depth={0}>{children}</DropdownMenuPanel>\n          </DropdownMenuViewport>\n        </BaseMenu.Popup>\n      </BaseMenu.Positioner>\n    </BaseMenu.Portal>\n  )\n}\n\nfunction DropdownMenuItem({\n  className,\n  ...props\n}: React.ComponentProps<typeof BaseMenu.Item>) {\n  const parked = useParked()\n\n  return (\n    <BaseMenu.Item\n      data-slot=\"dropdown-menu-item\"\n      className={cn(menuItemClass, className, parked)}\n      {...props}\n    />\n  )\n}\n\nfunction DropdownMenuGroup({\n  children,\n  ...props\n}: React.ComponentProps<typeof BaseMenu.Group>) {\n  // Not hidden when parked, unlike the parts inside it: a group can contain the\n  // sub whose level is on screen, and hiding it would hide that too. Its own\n  // items hide themselves.\n  return (\n    <BaseMenu.Group data-slot=\"dropdown-menu-group\" {...props}>\n      {children}\n    </BaseMenu.Group>\n  )\n}\n\n// A plain styled label so it works standalone in the menu (matching the\n// shadcn API). Base UI's Menu.GroupLabel requires a wrapping Menu.Group and\n// throws otherwise; use DropdownMenuGroup when you want a labelled group.\nfunction DropdownMenuLabel({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  const parked = useParked()\n\n  return (\n    <div\n      data-slot=\"dropdown-menu-label\"\n      className={cn(\n        \"px-2 py-1.5 text-xs font-medium text-muted-foreground\",\n        className,\n        parked\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction DropdownMenuSeparator({\n  className,\n  ...props\n}: React.ComponentProps<typeof BaseMenu.Separator>) {\n  const parked = useParked()\n\n  return (\n    <BaseMenu.Separator\n      data-slot=\"dropdown-menu-separator\"\n      className={cn(\"bg-border -mx-1 my-1 h-px\", className, parked)}\n      {...props}\n    />\n  )\n}\n\nfunction DropdownMenuCheckboxItem({\n  className,\n  children,\n  onCheckedChange,\n  ...props\n}: React.ComponentProps<typeof BaseMenu.CheckboxItem>) {\n  // Toggling a checkbox item commits state — fire the seam tick (§3b).\n  const { trigger } = useHaptics()\n  const parked = useParked()\n\n  return (\n    <BaseMenu.CheckboxItem\n      data-slot=\"dropdown-menu-checkbox-item\"\n      className={cn(menuItemClass, \"pl-8\", className, parked)}\n      onCheckedChange={(\n        ...args: Parameters<NonNullable<typeof onCheckedChange>>\n      ) => {\n        trigger(\"tick\")\n        onCheckedChange?.(...args)\n      }}\n      {...props}\n    >\n      <span className=\"absolute left-2 flex size-4 items-center justify-center\">\n        <BaseMenu.CheckboxItemIndicator>\n          <Check className=\"size-4\" strokeWidth={3} />\n        </BaseMenu.CheckboxItemIndicator>\n      </span>\n      {children}\n    </BaseMenu.CheckboxItem>\n  )\n}\n\nfunction DropdownMenuRadioGroup({\n  onValueChange,\n  children,\n  ...props\n}: React.ComponentProps<typeof BaseMenu.RadioGroup>) {\n  // Selecting a different radio item commits state — fire the seam tick (§3b).\n  const { trigger } = useHaptics()\n\n  // Never swapped out or hidden when parked: unmounting it would drop an\n  // uncontrolled `defaultValue`, and hiding it would hide a sub nested inside\n  // it. Its items hide themselves.\n  return (\n    <BaseMenu.RadioGroup\n      data-slot=\"dropdown-menu-radio-group\"\n      onValueChange={(\n        ...args: Parameters<NonNullable<typeof onValueChange>>\n      ) => {\n        trigger(\"tick\")\n        onValueChange?.(...args)\n      }}\n      {...props}\n    >\n      {children}\n    </BaseMenu.RadioGroup>\n  )\n}\n\nfunction DropdownMenuRadioItem({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof BaseMenu.RadioItem>) {\n  const parked = useParked()\n\n  return (\n    <BaseMenu.RadioItem\n      data-slot=\"dropdown-menu-radio-item\"\n      className={cn(menuItemClass, \"pl-8\", className, parked)}\n      {...props}\n    >\n      <span className=\"absolute left-2 flex size-4 items-center justify-center\">\n        <BaseMenu.RadioItemIndicator>\n          <Circle className=\"size-2 fill-current\" />\n        </BaseMenu.RadioItemIndicator>\n      </span>\n      {children}\n    </BaseMenu.RadioItem>\n  )\n}\n\n/**\n * Groups a nested level with the row that opens it. Nests to any depth — a\n * `DropdownMenuSub` inside a `DropdownMenuSubContent` is just the next level.\n */\nfunction DropdownMenuSub({ children }: { children?: React.ReactNode }) {\n  const id = React.useId()\n  const value = React.useMemo(() => ({ id }), [id])\n  return (\n    <DropdownSubContext.Provider value={value}>\n      {children}\n    </DropdownSubContext.Provider>\n  )\n}\n\nfunction DropdownMenuSubTrigger({\n  className,\n  children,\n  heading,\n  onClick,\n  onKeyDown,\n  ...props\n}: React.ComponentProps<typeof BaseMenu.Item> & {\n  /** Overrides what the nested level's back row is labelled with. */\n  heading?: React.ReactNode\n}) {\n  const sub = React.useContext(DropdownSubContext)\n  const { push } = useDropdownNav()\n  const { trigger } = useHaptics()\n  const parked = useParked()\n  if (!sub) return null\n\n  const drillIn = () => {\n    trigger(\"tap\")\n    push({ id: sub.id, label: heading ?? children })\n  }\n\n  return (\n    <BaseMenu.Item\n      data-slot=\"dropdown-menu-sub-trigger\"\n      data-sub-id={sub.id}\n      className={cn(menuItemClass, className, parked)}\n      // The popup stays open — the nested level replaces this one inside it.\n      closeOnClick={false}\n      onClick={(event) => {\n        onClick?.(event)\n        drillIn()\n      }}\n      onKeyDown={(event) => {\n        onKeyDown?.(event)\n        if (event.defaultPrevented) return\n        if (event.key === \"ArrowRight\") {\n          event.preventDefault()\n          drillIn()\n        }\n      }}\n      {...props}\n    >\n      {children}\n      <ChevronRight className=\"ml-auto size-4 text-muted-foreground\" />\n    </BaseMenu.Item>\n  )\n}\n\n/** The row that returns to the level below. Part of the roving focus order, so\n *  it's reachable by arrow keys as well as by ArrowLeft and Escape. */\nfunction DropdownMenuBack({ children }: { children?: React.ReactNode }) {\n  const { pop } = useDropdownNav()\n  const { trigger } = useHaptics()\n  const parked = useParked()\n\n  return (\n    <BaseMenu.Item\n      data-slot=\"dropdown-menu-back\"\n      className={cn(menuItemClass, \"gap-1.5 pl-1 font-medium\", parked)}\n      closeOnClick={false}\n      onClick={() => {\n        trigger(\"tap\")\n        pop()\n      }}\n    >\n      <ChevronLeft className=\"size-4 text-muted-foreground\" />\n      {children}\n    </BaseMenu.Item>\n  )\n}\n\nfunction DropdownMenuSubContent({\n  className,\n  children,\n}: {\n  className?: string\n  children?: React.ReactNode\n}) {\n  const sub = React.useContext(DropdownSubContext)\n  const { depth } = React.useContext(DropdownPanelContext)\n  const { path } = useDropdownNav()\n\n  // Mount only on the open path — one level per depth, and nothing below the\n  // one on screen.\n  const level = path[depth]\n  if (!sub || level?.id !== sub.id) return null\n\n  return (\n    <DropdownMenuPanel depth={depth + 1} className={className}>\n      <DropdownMenuBack>{level.label}</DropdownMenuBack>\n      <DropdownMenuSeparator />\n      {children}\n    </DropdownMenuPanel>\n  )\n}\n\nfunction DropdownMenuShortcut({\n  className,\n  ...props\n}: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"dropdown-menu-shortcut\"\n      className={cn(\n        \"text-muted-foreground ml-auto text-xs tracking-widest\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nexport {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuCheckboxItem,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuGroup,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuSub,\n  DropdownMenuSubTrigger,\n  DropdownMenuSubContent,\n  DropdownMenuShortcut,\n}\n",
      "type": "registry:ui",
      "target": "components/ui/dropdown-menu.tsx"
    }
  ]
}