{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dropdown-menu",
  "title": "Dropdown Menu",
  "description": "A menu that opens on mousedown 4px from its trigger, with one highlight gliding between rows and a macOS blink before it closes. Checkbox and radio rows, shortcuts and submenus share three aligned tracks.",
  "dependencies": [
    "@base-ui/react"
  ],
  "registryDependencies": [
    "https://eased.dev/r/eased-tokens.json",
    "https://eased.dev/r/eased-motion.json",
    "https://eased.dev/r/eased-floating.json",
    "https://eased.dev/r/eased-overlay-core.json",
    "https://eased.dev/r/button.json"
  ],
  "files": [
    {
      "path": "src/registry/eased/dropdown-menu/dropdown-menu.tsx",
      "content": "\"use client\";\n\nimport { Menu } from \"@base-ui/react/menu\";\nimport {\n  createContext,\n  use,\n  useEffect,\n  useMemo,\n  useRef,\n  type ComponentProps,\n  type ReactNode,\n  type RefObject,\n} from \"react\";\nimport { FloatingLayer, type FloatingAlign, type FloatingSide } from \"../_primitives/floating/floating\";\nimport {\n  flashThenClose,\n  groupClass,\n  groupLabelClass,\n  HighlightGroup,\n  ItemCheck,\n  ItemChevron,\n  itemClass,\n  ItemParts,\n  menuSurface,\n  OVERLAY_ROLES,\n  separatorClass,\n  withHighlight,\n} from \"../_primitives/overlay-core/overlay-core\";\nimport { Button, type ButtonSize, type ButtonVariant } from \"../button/button\";\nimport { useSafeReducedMotion } from \"../lib/motion\";\n\n/**\n * A dropdown menu on Base UI's Menu. It opens on mousedown from its trigger's transform-origin,\n * 4px toward the trigger at 0.97 in 150ms, and leaves half as far in 100ms. One highlight glides\n * between rows on the snappy spring; a chosen row blinks (40ms off, 40ms on) before the menu\n * closes, as on macOS. Submenus open beside their row with their first row on the row's line.\n *\n * The row parts (`DropdownMenuItem` … `DropdownMenuSeparator`) also serve Context Menu: Base UI's\n * ContextMenu re-exports Menu's parts, so the same rows work under either root.\n */\n\n/* ---------------------------------------------------------- Selection flash ---------- */\n\ntype Flash = { close: () => void; reduced: boolean };\n\n/** Lets a row close the whole menu after its flash. Provided by `DropdownMenu` and `ContextMenu`. */\nexport const MenuFlashContext = createContext<Flash | null>(null);\n\ntype Actions = { close: () => void; unmount: () => void };\n\n/**\n * The root's half of the selection flash: an actions ref for Base UI's root (yours, if you pass\n * one) and the context value rows read to close it. For roots that wrap Menu or ContextMenu.\n */\nexport function useMenuFlash(actionsRef?: RefObject<Actions | null>) {\n  const own = useRef<Actions | null>(null);\n  const ref = actionsRef ?? own;\n  const reduced = useSafeReducedMotion();\n  const flash = useMemo<Flash>(() => ({ close: () => ref.current?.close(), reduced }), [ref, reduced]);\n  return { ref, flash };\n}\n\n/* -------------------------------------------------------------------- Root ---------- */\n\nexport type DropdownMenuProps = ComponentProps<typeof Menu.Root>;\n\n/**\n * Groups a menu's parts and owns its open state (`open` / `defaultOpen` / `onOpenChange`). Modal by\n * default: page scroll locks and outside clicks only dismiss. It also lets rows flash before it\n * closes; `actionsRef` still reaches Base UI's `close` and `unmount`.\n */\nexport function DropdownMenu({ actionsRef, children, ...props }: DropdownMenuProps) {\n  const { ref, flash } = useMenuFlash(actionsRef);\n  return (\n    <MenuFlashContext value={flash}>\n      <Menu.Root {...props} actionsRef={ref}>\n        {children}\n      </Menu.Root>\n    </MenuFlashContext>\n  );\n}\n\nexport type DropdownMenuTriggerProps = Omit<ComponentProps<typeof Menu.Trigger>, \"className\"> & {\n  /** The Eased `Button` variant. Default \"secondary\"; \"ghost\" for a quiet \"⋯\" in a header. */\n  variant?: ButtonVariant;\n  /** The Eased `Button` size. Default \"sm\" (32px); `icon-sm` for a square icon trigger. */\n  size?: ButtonSize;\n  className?: string;\n};\n\n/**\n * The button that opens the menu, on mousedown (rule 71), so press-drag-release picks a row in one\n * gesture. An Eased `Button`: it never scales as a trigger (R64) and stays pressed\n * (`aria-expanded`) while the menu is open. With `openOnHover` it waits 300ms after the pointer\n * leaves before closing (`closeDelay`). Pass `render` for your own element (style it with `floatingTrigger`).\n */\nexport function DropdownMenuTrigger({\n  variant = \"secondary\",\n  size = \"sm\",\n  className = \"\",\n  render,\n  openOnHover,\n  closeDelay = openOnHover ? 300 : 0,\n  ...props\n}: DropdownMenuTriggerProps) {\n  return (\n    <Menu.Trigger\n      {...props}\n      openOnHover={openOnHover}\n      closeDelay={closeDelay}\n      render={render ?? <Button variant={variant} size={size} className={className} />}\n    />\n  );\n}\n\n/* ----------------------------------------------------------------- Content ---------- */\n\nexport type DropdownMenuContentProps = {\n  /** Preferred side; flips on collision and the enter direction follows. Default \"bottom\". */\n  side?: FloatingSide;\n  /** Default \"start\": the menu's leading edge on the trigger's. */\n  align?: FloatingAlign;\n  /** Gap to the trigger, px. Default 4. */\n  sideOffset?: number;\n  alignOffset?: number;\n  /** Where focus returns on close. Default: the trigger. */\n  finalFocus?: ComponentProps<typeof Menu.Popup>[\"finalFocus\"];\n  /** Portal target. Default `document.body`. */\n  container?: HTMLElement | null;\n  /** Stacking order. Default 50; raise it inside a higher layer such as a dialog. */\n  zIndex?: number;\n  /** A CSS width, e.g. `\"15rem\"`. Default: fits the rows, at least 192px, never wider than the viewport. */\n  width?: string;\n  /** Appended to the popup's classes. */\n  className?: string;\n  children?: ReactNode;\n};\n\n/**\n * The menu popup: a raised 16px panel with 4px padding whose rows share three tracks (icon ·\n * label · trailing), so labels start on one x and shortcuts, checks and chevrons share a column.\n * It scrolls within the space available.\n */\nexport function DropdownMenuContent({\n  side = OVERLAY_ROLES.menu.side,\n  align = \"start\",\n  sideOffset = OVERLAY_ROLES.menu.sideOffset,\n  alignOffset,\n  finalFocus,\n  container,\n  zIndex,\n  width,\n  className = \"\",\n  children,\n}: DropdownMenuContentProps) {\n  return (\n    <FloatingLayer\n      parts={Menu}\n      motion={OVERLAY_ROLES.menu}\n      side={side}\n      align={align}\n      sideOffset={sideOffset}\n      alignOffset={alignOffset}\n      container={container}\n      zIndex={zIndex}\n      popupProps={{ finalFocus }}\n      style={width ? { width } : undefined}\n      className={`${menuSurface} ${className}`}\n    >\n      <HighlightGroup>{children}</HighlightGroup>\n    </FloatingLayer>\n  );\n}\n\n/* -------------------------------------------------------------------- Rows ---------- */\n\ntype RowContent = {\n  /** 16px icon in the leading track; muted at rest, full colour while highlighted. */\n  icon?: ReactNode;\n  /** Shortcut glyphs for the trailing column, e.g. `\"⌘D\"`. Display only: bind the key yourself. */\n  shortcut?: string;\n  /** A second, muted line under the label. */\n  description?: ReactNode;\n  /** Appended to the row's classes. */\n  className?: string;\n  /** The label. */\n  children: ReactNode;\n};\n\nexport type DropdownMenuItemProps = Omit<ComponentProps<typeof Menu.Item>, \"className\" | \"children\" | \"render\"> &\n  RowContent & {\n    /** `\"danger\"`: red label and icon, a red-tinted highlight. For destructive actions. Default \"default\". */\n    tone?: \"default\" | \"danger\";\n  };\n\n/**\n * A command row. Choosing it runs `onClick`, then the row blinks for 80ms and the menu closes\n * (at once under reduced motion). `closeOnClick={false}` keeps the menu open and skips the blink.\n * `disabled` rows stay in the list and in keyboard order (so they can be read), and ignore clicks.\n */\nexport function DropdownMenuItem({\n  icon,\n  shortcut,\n  description,\n  tone = \"default\",\n  closeOnClick = true,\n  onClick,\n  className = \"\",\n  children,\n  ...props\n}: DropdownMenuItemProps) {\n  const flash = use(MenuFlashContext);\n  const cancel = useRef<() => void>(undefined);\n  useEffect(() => () => cancel.current?.(), []);\n  // Without a flash-aware root (a bare Base UI Menu.Root), Base UI closes the menu itself.\n  const flashes = flash !== null && closeOnClick;\n\n  return (\n    <Menu.Item\n      {...props}\n      closeOnClick={flashes ? false : closeOnClick}\n      onClick={(event) => {\n        onClick?.(event);\n        if (!flash || !flashes || event.baseUIHandlerPrevented) return;\n        cancel.current?.();\n        cancel.current = flashThenClose(event.currentTarget, flash.close, flash.reduced);\n      }}\n      data-tone={tone === \"danger\" ? \"danger\" : undefined}\n      className={`${itemClass({ tone, glide: true })} ${className}`}\n      render={withHighlight}\n    >\n      <ItemParts icon={icon} shortcut={shortcut} description={description}>\n        {children}\n      </ItemParts>\n    </Menu.Item>\n  );\n}\n\nexport type DropdownMenuLinkItemProps = Omit<ComponentProps<typeof Menu.LinkItem>, \"className\" | \"children\" | \"render\"> &\n  RowContent;\n\n/** A row that navigates: an `<a>` with `href`. The page change is the feedback, so it doesn't blink. */\nexport function DropdownMenuLinkItem({ icon, shortcut, description, className = \"\", children, ...props }: DropdownMenuLinkItemProps) {\n  return (\n    <Menu.LinkItem\n      {...props}\n      className={`${itemClass({ glide: true })} ${className}`}\n      render={withHighlight}\n    >\n      <ItemParts icon={icon} shortcut={shortcut} description={description}>\n        {children}\n      </ItemParts>\n    </Menu.LinkItem>\n  );\n}\n\nexport type DropdownMenuCheckboxItemProps = Omit<ComponentProps<typeof Menu.CheckboxItem>, \"className\" | \"children\" | \"render\"> &\n  Omit<RowContent, \"icon\">;\n\n/**\n * A toggle row (`checked` / `defaultChecked` / `onCheckedChange`). Its check sits in the icon\n * track and draws in when ticked. The menu stays open on click so you can set several.\n */\nexport function DropdownMenuCheckboxItem({ shortcut, description, className = \"\", children, ...props }: DropdownMenuCheckboxItemProps) {\n  return (\n    <Menu.CheckboxItem {...props} className={`${itemClass({ glide: true })} ${className}`} render={withHighlight}>\n      <ItemParts\n        icon={\n          <Menu.CheckboxItemIndicator keepMounted className=\"flex\">\n            <ItemCheck />\n          </Menu.CheckboxItemIndicator>\n        }\n        shortcut={shortcut}\n        description={description}\n      >\n        {children}\n      </ItemParts>\n    </Menu.CheckboxItem>\n  );\n}\n\nexport type DropdownMenuRadioGroupProps = Omit<ComponentProps<typeof Menu.RadioGroup>, \"className\"> & { className?: string };\n\n/** One choice among its `DropdownMenuRadioItem`s (`value` / `defaultValue` / `onValueChange`). Keeps the rows on the menu's tracks. */\nexport function DropdownMenuRadioGroup({ className = \"\", ...props }: DropdownMenuRadioGroupProps) {\n  return <Menu.RadioGroup {...props} className={`${groupClass} ${className}`} />;\n}\n\nexport type DropdownMenuRadioItemProps = Omit<ComponentProps<typeof Menu.RadioItem>, \"className\" | \"children\" | \"render\"> &\n  Omit<RowContent, \"icon\">;\n\n/** One option of a radio group (`value` required). The check marks the chosen one; the menu stays open on click. */\nexport function DropdownMenuRadioItem({ shortcut, description, className = \"\", children, ...props }: DropdownMenuRadioItemProps) {\n  return (\n    <Menu.RadioItem {...props} className={`${itemClass({ glide: true })} ${className}`} render={withHighlight}>\n      <ItemParts\n        icon={\n          <Menu.RadioItemIndicator keepMounted className=\"flex\">\n            <ItemCheck />\n          </Menu.RadioItemIndicator>\n        }\n        shortcut={shortcut}\n        description={description}\n      >\n        {children}\n      </ItemParts>\n    </Menu.RadioItem>\n  );\n}\n\n/* ---------------------------------------------------------------- Submenus ---------- */\n\n/** A nested menu: holds one `DropdownMenuSubTrigger` and one `DropdownMenuSubContent`. Base UI `Menu.SubmenuRoot`. */\nexport const DropdownMenuSub = Menu.SubmenuRoot;\n\nexport type DropdownMenuSubTriggerProps = Omit<ComponentProps<typeof Menu.SubmenuTrigger>, \"className\" | \"children\" | \"render\"> &\n  Omit<RowContent, \"shortcut\">;\n\n/**\n * The row that opens a submenu: a chevron in the trailing column. It opens on hover (100ms\n * `delay`), click, Enter or ArrowRight; Base UI's safe-hover triangle keeps it open while the\n * pointer travels diagonally to it. While the submenu is open the row keeps a quiet fill.\n */\nexport function DropdownMenuSubTrigger({ icon, description, className = \"\", children, ...props }: DropdownMenuSubTriggerProps) {\n  return (\n    <Menu.SubmenuTrigger\n      {...props}\n      className={`${itemClass({ glide: true })} data-popup-open:bg-surface-hover ${className}`}\n      render={withHighlight}\n    >\n      <ItemParts icon={icon} description={description} trailing={<ItemChevron />}>\n        {children}\n      </ItemParts>\n    </Menu.SubmenuTrigger>\n  );\n}\n\n/** The menu's 1px border plus its 4px padding: how far a submenu rises so its first row sits on its trigger row. */\nconst SUBMENU_ALIGN_OFFSET = -5;\n\nexport type DropdownMenuSubContentProps = Pick<DropdownMenuContentProps, \"container\" | \"zIndex\" | \"width\" | \"className\" | \"children\">;\n\n/**\n * The submenu popup, beside its row (flipping to the other side on collision), raised by the\n * menu's border and padding so its first row lines up with the trigger row. It slides 4px out\n * from the parent and has its own gliding highlight.\n */\nexport function DropdownMenuSubContent({ container, zIndex, width, className = \"\", children }: DropdownMenuSubContentProps) {\n  return (\n    <FloatingLayer\n      parts={Menu}\n      motion={OVERLAY_ROLES.submenu}\n      side={OVERLAY_ROLES.submenu.side}\n      align=\"start\"\n      sideOffset={OVERLAY_ROLES.submenu.sideOffset}\n      alignOffset={SUBMENU_ALIGN_OFFSET}\n      container={container}\n      zIndex={zIndex}\n      style={width ? { width } : undefined}\n      className={`${menuSurface} ${className}`}\n    >\n      <HighlightGroup>{children}</HighlightGroup>\n    </FloatingLayer>\n  );\n}\n\n/* ---------------------------------------------------------------- Grouping ---------- */\n\nexport type DropdownMenuGroupProps = Omit<ComponentProps<typeof Menu.Group>, \"className\"> & { className?: string };\n\n/** Related rows, labelled by a `DropdownMenuLabel` inside it. Keeps its rows on the menu's tracks. */\nexport function DropdownMenuGroup({ className = \"\", ...props }: DropdownMenuGroupProps) {\n  return <Menu.Group {...props} className={`${groupClass} ${className}`} />;\n}\n\nexport type DropdownMenuLabelProps = Omit<ComponentProps<typeof Menu.GroupLabel>, \"className\"> & { className?: string };\n\n/** A group's heading: 12px, muted, 28px tall. Names its `DropdownMenuGroup` or `DropdownMenuRadioGroup`. */\nexport function DropdownMenuLabel({ className = \"\", ...props }: DropdownMenuLabelProps) {\n  return <Menu.GroupLabel {...props} className={`${groupLabelClass} ${className}`} />;\n}\n\n/** A full-bleed 1px rule between groups. */\nexport function DropdownMenuSeparator({ className = \"\" }: { className?: string }) {\n  return <Menu.Separator className={`${separatorClass} ${className}`} />;\n}\n",
      "type": "registry:ui",
      "target": "@components/eased/dropdown-menu/dropdown-menu.tsx"
    }
  ],
  "meta": {
    "industries": [
      "saas",
      "devtools",
      "legal",
      "hr"
    ],
    "releaseAt": "2026-10-18"
  },
  "type": "registry:ui"
}