{
  "schemaVersion": 1,
  "target": "react",
  "name": "dropdown-menu",
  "version": "0.2.0",
  "category": "core",
  "title": "Dropdown Menu",
  "description": "A menu of actions opened from a button, built on Base UI Menu, with groups, separators, shortcuts, icons, and disabled items.",
  "tags": [
    "overlay",
    "menu",
    "actions",
    "base-ui"
  ],
  "requires": {
    "projectTypes": [
      "next",
      "vite-react"
    ],
    "styling": "tailwind-v4"
  },
  "npmDependencies": {
    "@base-ui/react": "^1.9.0"
  },
  "registryDependencies": [],
  "docsPath": "/components/dropdown-menu",
  "props": [
    {
      "name": "DropdownMenu open / defaultOpen / onOpenChange",
      "type": "boolean / boolean / (open, details) => void",
      "description": "Root state. Leave open unset for an uncontrolled menu."
    },
    {
      "name": "DropdownMenu loopFocus",
      "type": "boolean",
      "default": "true",
      "description": "Wraps arrow-key focus from the last item to the first."
    },
    {
      "name": "DropdownMenu modal",
      "type": "boolean",
      "default": "true",
      "description": "Locks page scroll and blocks outside pointer interaction while open."
    },
    {
      "name": "DropdownMenuTrigger",
      "type": "Base UI Menu.Trigger props",
      "description": "Unstyled button that opens the menu. Pass className, or render={<Button />}. Sets data-popup-open while open."
    },
    {
      "name": "DropdownMenuContent side / align",
      "type": "Side / \"start\" | \"center\" | \"end\"",
      "default": "\"bottom\" / \"start\"",
      "description": "Placement against the trigger. Flips when there is not enough room."
    },
    {
      "name": "DropdownMenuContent sideOffset / alignOffset",
      "type": "number",
      "default": "6 / 0",
      "description": "Gap from the trigger and shift along its edge, in pixels."
    },
    {
      "name": "DropdownMenuItem onClick",
      "type": "(event) => void",
      "description": "Runs the action. The menu closes afterwards unless closeOnClick is false."
    },
    {
      "name": "DropdownMenuItem disabled",
      "type": "boolean",
      "default": "false",
      "description": "Keeps the item visible and reachable with the arrow keys, but ignores clicks and Enter."
    },
    {
      "name": "DropdownMenuItem destructive",
      "type": "boolean",
      "default": "false",
      "description": "Red styling for delete-style actions."
    },
    {
      "name": "DropdownMenuItem label",
      "type": "string",
      "description": "Text used for typeahead when the item content is not plain text."
    },
    {
      "name": "DropdownMenuGroup / DropdownMenuLabel",
      "type": "Base UI Menu.Group / Menu.GroupLabel props",
      "description": "Groups items; the label must sit inside the group it names."
    },
    {
      "name": "DropdownMenuSeparator",
      "type": "Base UI Separator props",
      "description": "Divider between groups."
    },
    {
      "name": "DropdownMenuShortcut",
      "type": "ComponentProps<\"span\">",
      "description": "Right-aligned keyboard shortcut hint inside an item, set in mono."
    }
  ],
  "accessibility": [
    "The trigger sets aria-haspopup and aria-expanded; the panel has role=\"menu\" and items have role=\"menuitem\".",
    "Enter, Space, or ArrowDown on the trigger opens the menu with focus on the first item; Arrow keys, Home, and End move between items.",
    "Typing a letter jumps to the next item starting with it (typeahead).",
    "Disabled items set aria-disabled and stay reachable with the arrow keys, so screen reader users know the action exists; activating them does nothing.",
    "Escape or choosing an item closes the menu and returns focus to the trigger.",
    "Groups are labelled with aria-labelledby from DropdownMenuLabel; separators have role=\"separator\".",
    "The highlighted item changes background and text color instantly (no animated highlight, so arrow-key movement never lags). The panel grows from the trigger in 160ms and fades out in 110ms; with reduced motion the transitions are removed."
  ],
  "states": [
    "closed",
    "open"
  ],
  "files": [
    {
      "path": "ui/dropdown-menu.tsx",
      "content": "\"use client\";\n\nimport { Menu as BaseMenu } from \"@base-ui/react/menu\";\nimport type { ComponentProps } from \"react\";\n\ntype ClassName<State> = string | ((state: State) => string | undefined) | undefined;\n\nfunction cx(...classes: (string | false | null | undefined)[]) {\n  return classes.filter(Boolean).join(\" \");\n}\n\n/** Prepends base classes while keeping Base UI's state-function form of className working. */\nfunction withBase<State>(base: string, className: ClassName<State>) {\n  return typeof className === \"function\" ? (state: State) => cx(base, className(state)) : cx(base, className);\n}\n\n/** Groups the menu parts and holds its open state. Pass `open`/`onOpenChange` to control it. */\nexport const DropdownMenu = BaseMenu.Root;\n\n/** A button that opens the menu. Unstyled; pass `className`, or `render={<Button />}` to reuse your button. */\nexport const DropdownMenuTrigger = BaseMenu.Trigger;\n\n/** Groups related items. Put a DropdownMenuLabel inside to name the group. */\nexport const DropdownMenuGroup = BaseMenu.Group;\n\ntype PositionerProps = BaseMenu.Positioner.Props;\n\nexport interface DropdownMenuContentProps extends BaseMenu.Popup.Props {\n  /** Side of the trigger to place the menu on. Flips when there is not enough room. */\n  side?: PositionerProps[\"side\"];\n  /** Alignment against the trigger on the chosen side. */\n  align?: PositionerProps[\"align\"];\n  /** Gap between the trigger and the menu, in pixels. */\n  sideOffset?: PositionerProps[\"sideOffset\"];\n  /** Shift along the trigger edge, in pixels. */\n  alignOffset?: PositionerProps[\"alignOffset\"];\n  /** Classes for the positioner wrapper, for example to change the z-index. */\n  positionerClassName?: string;\n  /** Element to portal into. Defaults to `document.body`. */\n  container?: BaseMenu.Portal.Props[\"container\"];\n}\n\n/** The menu panel, rendered in a portal and positioned against the trigger. */\nexport function DropdownMenuContent({\n  side = \"bottom\",\n  align = \"start\",\n  sideOffset = 6,\n  alignOffset = 0,\n  positionerClassName,\n  container,\n  className,\n  ...props\n}: DropdownMenuContentProps) {\n  return (\n    <BaseMenu.Portal container={container}>\n      <BaseMenu.Positioner\n        side={side}\n        align={align}\n        sideOffset={sideOffset}\n        alignOffset={alignOffset}\n        className={cx(\"z-50 outline-none\", positionerClassName)}\n      >\n        <BaseMenu.Popup\n          className={withBase(\n            cx(\n              // 12px panel radius minus 6px padding gives the items their concentric 6px radius.\n              \"max-h-(--available-height) min-w-52 origin-(--transform-origin) overflow-y-auto overscroll-contain rounded-xl p-1.5 outline-none\",\n              \"border border-zinc-950/10 bg-white text-zinc-950 shadow-[0_12px_32px_-8px_rgb(0_0_0/0.18)]\",\n              \"dark:border-white/15 dark:bg-black dark:text-white dark:shadow-[0_16px_48px_-12px_rgb(0_0_0/1),inset_0_1px_0_rgb(255_255_255/0.08)]\",\n              // Enter: 160ms from 96% at the trigger. Exit: 110ms, fades with a smaller scale.\n              \"transition-[opacity,scale] duration-160 ease-[cubic-bezier(0.23,1,0.32,1)] motion-reduce:transition-none\",\n              \"data-[starting-style]:opacity-0 motion-safe:data-[starting-style]:scale-96\",\n              \"data-[ending-style]:opacity-0 data-[ending-style]:duration-110 data-[ending-style]:ease-out motion-safe:data-[ending-style]:scale-98\",\n            ),\n            className,\n          )}\n          {...props}\n        />\n      </BaseMenu.Positioner>\n    </BaseMenu.Portal>\n  );\n}\n\nexport interface DropdownMenuItemProps extends BaseMenu.Item.Props {\n  /** Styles the item for actions that delete or cannot be undone. */\n  destructive?: boolean;\n}\n\n/**\n * An action in the menu. Use `onClick` to handle it; `disabled` keeps it visible but skips it.\n * The highlight follows the pointer and arrow keys instantly; menus are used too often to animate it.\n */\nexport function DropdownMenuItem({ destructive = false, className, ...props }: DropdownMenuItemProps) {\n  return (\n    <BaseMenu.Item\n      data-destructive={destructive || undefined}\n      className={withBase(\n        cx(\n          \"flex h-8 cursor-default items-center gap-2.5 rounded-md px-2 text-sm outline-none select-none\",\n          \"[&_svg]:size-4 [&_svg]:shrink-0\",\n          \"data-[disabled]:pointer-events-none data-[disabled]:opacity-45\",\n          destructive\n            ? cx(\n                \"text-red-600 [&_svg]:text-red-600/80 data-[highlighted]:bg-red-500/[0.08] data-[highlighted]:text-red-700\",\n                \"dark:text-red-400 dark:[&_svg]:text-red-400/80 dark:data-[highlighted]:bg-red-500/[0.12] dark:data-[highlighted]:text-red-300\",\n              )\n            : cx(\n                \"text-zinc-700 [&_svg]:text-zinc-500 data-[highlighted]:bg-zinc-950/[0.05] data-[highlighted]:text-zinc-950 data-[highlighted]:[&_svg]:text-zinc-950\",\n                \"dark:text-zinc-300 dark:[&_svg]:text-zinc-500 dark:data-[highlighted]:bg-white/[0.08] dark:data-[highlighted]:text-white dark:data-[highlighted]:[&_svg]:text-white\",\n              ),\n        ),\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\n/** Names a DropdownMenuGroup. Must be rendered inside the group it labels. */\nexport function DropdownMenuLabel({ className, ...props }: BaseMenu.GroupLabel.Props) {\n  return (\n    <BaseMenu.GroupLabel\n      className={withBase(\"px-2 pt-2 pb-1 text-xs font-medium text-zinc-500 select-none\", className)}\n      {...props}\n    />\n  );\n}\n\n/** A divider between groups of items. */\nexport function DropdownMenuSeparator({ className, ...props }: ComponentProps<typeof BaseMenu.Separator>) {\n  return (\n    <BaseMenu.Separator\n      className={withBase(\"-mx-1.5 my-1.5 h-px bg-zinc-950/[0.07] dark:bg-white/[0.08]\", className)}\n      {...props}\n    />\n  );\n}\n\n/** A keyboard shortcut hint shown at the end of an item. */\nexport function DropdownMenuShortcut({ className, ...props }: ComponentProps<\"span\">) {\n  return (\n    <span\n      className={cx(\n        \"ml-auto pl-6 font-mono text-xs text-zinc-400 tabular-nums dark:text-zinc-500\",\n        \"in-data-[highlighted]:text-zinc-500 dark:in-data-[highlighted]:text-zinc-400\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n"
    }
  ],
  "example": {
    "content": "import {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuTrigger,\n} from \"@/components/seamless/ui/dropdown-menu\";\n\n// Same recipe as the Button component's secondary variant, inlined so this example has no other dependency.\nconst button =\n  \"group inline-flex h-9 items-center justify-center gap-1.5 rounded-lg border border-zinc-950/12 bg-white pr-2.5 pl-3.5 text-sm font-medium text-zinc-950 select-none \" +\n  \"transition-[background-color,border-color,scale] duration-150 ease-out active:duration-100 motion-safe:active:scale-[0.97] motion-reduce:transition-none \" +\n  \"hover:border-zinc-950/20 hover:bg-zinc-950/[0.03] data-[popup-open]:bg-zinc-950/[0.04] \" +\n  \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-500 \" +\n  \"dark:border-white/15 dark:bg-black dark:text-white dark:hover:border-white/25 dark:hover:bg-white/[0.06] dark:data-[popup-open]:bg-white/[0.06]\";\n\nconst icon = { \"aria-hidden\": true, viewBox: \"0 0 16 16\", fill: \"none\" } as const;\nconst stroke = { stroke: \"currentColor\", strokeWidth: 1.5, strokeLinecap: \"round\", strokeLinejoin: \"round\" } as const;\n\nexport default function DropdownMenuExample() {\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger className={button}>\n        Options\n        <svg\n          {...icon}\n          className=\"size-4 text-zinc-500 transition-[rotate] duration-160 ease-[cubic-bezier(0.23,1,0.32,1)] group-data-[popup-open]:rotate-180 motion-reduce:transition-none\"\n        >\n          <path d=\"m4.5 6.25 3.5 3.5 3.5-3.5\" {...stroke} />\n        </svg>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent>\n        <DropdownMenuItem>\n          <svg {...icon}>\n            <path d=\"M9.5 3.5l3 3-7 7h-3v-3l7-7z\" {...stroke} />\n          </svg>\n          Rename\n          <DropdownMenuShortcut>⌘R</DropdownMenuShortcut>\n        </DropdownMenuItem>\n        <DropdownMenuItem>\n          <svg {...icon}>\n            <rect x=\"5.5\" y=\"5.5\" width=\"8\" height=\"8\" rx=\"1.5\" {...stroke} />\n            <path d=\"M10.5 3.5v-.25A.75.75 0 0 0 9.75 2.5h-6.5a.75.75 0 0 0-.75.75v6.5c0 .41.34.75.75.75h.25\" {...stroke} />\n          </svg>\n          Duplicate\n          <DropdownMenuShortcut>⌘D</DropdownMenuShortcut>\n        </DropdownMenuItem>\n        <DropdownMenuSeparator />\n        <DropdownMenuGroup>\n          <DropdownMenuLabel>Share</DropdownMenuLabel>\n          <DropdownMenuItem>\n            <svg {...icon}>\n              <path d=\"M7 9a2.5 2.5 0 0 0 3.5 0l2-2a2.5 2.5 0 0 0-3.5-3.5l-.5.5M9 7a2.5 2.5 0 0 0-3.5 0l-2 2A2.5 2.5 0 0 0 7 12.5l.5-.5\" {...stroke} />\n            </svg>\n            Copy link\n          </DropdownMenuItem>\n          <DropdownMenuItem disabled>\n            <svg {...icon}>\n              <circle cx=\"8\" cy=\"8\" r=\"5.75\" {...stroke} />\n              <path d=\"M2.5 8h11M8 2.25c1.5 1.6 2.25 3.5 2.25 5.75S9.5 12.15 8 13.75C6.5 12.15 5.75 10.25 5.75 8S6.5 3.85 8 2.25z\" {...stroke} />\n            </svg>\n            Publish to web\n          </DropdownMenuItem>\n        </DropdownMenuGroup>\n        <DropdownMenuSeparator />\n        <DropdownMenuItem destructive>\n          <svg {...icon}>\n            <path d=\"M3 4.5h10M6.5 4.5V3a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v1.5M4.5 4.5l.6 8.1a1 1 0 0 0 1 .9h3.8a1 1 0 0 0 1-.9l.6-8.1\" {...stroke} />\n          </svg>\n          Delete\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n"
  }
}
