{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "menu",
  "title": "Menu",
  "description": "Anchored action menu with flat commands, grouped sections, single-selection choices, supplemental notification dots or trailing tags, prefix icons, reorderable rows, switches, and destructive actions.",
  "type": "registry:ui",
  "meta": {
    "library": "@ds-mo/ui",
    "distribution": "npm",
    "storybook": "https://zainadeel.github.io/compomo/?path=/story/menu",
    "source": "src/wc/components/Menu/Menu.tsx",
    "intentStatus": "complete",
    "intent": {
      "audience": "general",
      "status": "stable",
      "summary": "Anchored action menu with flat commands, grouped sections, single-selection choices, supplemental notification dots or trailing tags, prefix icons, reorderable rows, switches, and destructive actions.",
      "useWhen": [
        "A trigger reveals a short contextual list of commands or state-changing actions.",
        "Actions need grouping, mutually exclusive selected state, supplemental notification dots or trailing tags, prefix icons, reorderable switch or command rows, or destructive treatment."
      ],
      "avoidWhen": [
        "The user is selecting a value for a form field.",
        "The content requires rich dialog interaction or a multi-step workflow.",
        "The actions should remain persistently visible."
      ],
      "alternatives": [
        {
          "when": "The choice is a form value with a label, validation, and submitted value.",
          "component": "component:ds-select",
          "reason": "Select provides form-control semantics and value ownership."
        },
        {
          "when": "Several form values may be selected.",
          "component": "component:ds-select",
          "reason": "Select multiple provides multi-select listbox semantics, array value ownership, and count presentation."
        },
        {
          "when": "The interaction needs substantial content, confirmation, or a form.",
          "component": "component:ds-modal",
          "reason": "Modal owns focus containment and dialog-level interaction."
        }
      ],
      "commonlyComposedWith": [
        "component:ds-button-unfilled",
        "component:ds-panel-nav",
        "component:ds-switch",
        "component:ds-swatch-picker"
      ],
      "patterns": [
        "pattern:menu-trigger"
      ],
      "accessibility": [
        "Anchor the menu to the control that opens it and return focus to that control when the menu closes.",
        "Open with visible initial focus only when the trigger was activated from the keyboard.",
        "Keep disabled items discoverable only when their unavailable state is meaningful.",
        "Switch rows use menuitemcheckbox semantics on the row; the nested ds-switch is an aria-hidden presentation indicator at the same density as the menu.",
        "Single-selection menus use menuitemradio semantics and aria-checked on each row, but render no nested radio control; selected interaction fill and primary foreground provide the visual state.",
        "A swatch-picker section changes the popup to a non-modal dialog so its radio group and ordinary preference buttons keep valid nested semantics; Tab moves between its controls, then leaves and closes at the popup boundary.",
        "Use notification dots only as supplemental status and not as the sole indication of important information.",
        "Trailing tags are non-interactive supplemental content; their visible label remains part of the menu item's accessible name.",
        "Reorderable rows keep a presentation Drag handle. Pointer drag on the handle and Alt+Arrow Up or Down emit dsReorder without closing. Pointer cancellation abandons the pending move. Hide-locked reorderable rows expose aria-disabled for the unavailable toggle but stay focusable so they can move; fully locked rows stay out of the contiguous reorderable run.",
        "Announce the new position through a polite live region that is a sibling of the role=menu popup, so the menu retains only permitted descendants."
      ],
      "states": [
        "The host owns open state and responds to selection and close events.",
        "Use either flat items or sections, not both.",
        "After dsSelect from a switch row, replace the items or sections array with updated switchValue data so the controlled indicator reflects the new state.",
        "After dsReorder, replace items or the affected section with the emitted items array. Menu never mutates order and does not close.",
        "For selectionMode single, keep exactly one item isSelected and replace the items or sections array after dsSelect; Menu never mutates consumer-owned selection data.",
        "Arrays, objects, and element anchors must be assigned as JavaScript properties.",
        "Use a swatch-picker section for a small curated visual preference and apply dsSwatchSelect immediately without closing the popup.",
        "Ordinary rows, section headings, and supporting text consume the Menu size. At xs, both the primary label and supporting text use caption metrics.",
        "A trailing tag resolves one density smaller than its menu row and uses the inset recipe so it nests without changing row height. Do not combine tag, dot, and switch suffixes on one item.",
        "Reorderable rows use the Drag prefix and ignore icon. Prefix and drag-handle icons inherit the choice-popup secondary foreground, matching Select. When any row in a section has an icon or is reorderable, the remaining rows get a spacer so labels align."
      ],
      "responsiveBehavior": [
        "Keep the default choice-cell anchor alignment so the first or last interactive row edge aligns with its trigger, matching Select; use popup-frame only for deliberate custom frame geometry.",
        "Treat side as the preferred placement. When the popup cannot fit there and the opposite side offers more room, Menu flips on that main axis before clamping the final position to the viewport.",
        "Avoid fixed widths that exceed narrow viewports."
      ],
      "frameworkCaveats": {
        "customElements": [
          "Assign items, sections, and HTMLElement anchors as properties after the element is defined.",
          "Handle dsReorder by replacing the items or section items with the emitted items array; Menu never mutates order."
        ],
        "react": [
          "Pass arrays and objects as wrapper props and handle generated onDsSelect, onDsReorder, and onDsClose event props."
        ],
        "angular": [
          "Use property bindings for arrays, objects, and anchors and event bindings for dsSelect, dsReorder, and dsClose."
        ]
      },
      "references": [
        {
          "label": "Storybook examples",
          "path": "src/wc/components/Menu/Menu.stories.ts"
        },
        {
          "label": "Framework integration",
          "path": "docs/framework-integration.md"
        }
      ]
    },
    "api": {
      "props": {
        "align": {
          "type": "MenuAlign",
          "resolvedType": "\"center\" | \"end\" | \"start\"",
          "attribute": "align",
          "default": "'start'",
          "required": false,
          "mutable": false
        },
        "alignOffset": {
          "type": "number | string",
          "resolvedType": "number | string",
          "attribute": "align-offset",
          "default": "0",
          "required": false,
          "mutable": false,
          "description": "Cross-axis offset — number (px) or TokoMo length."
        },
        "anchor": {
          "type": "HTMLElement | undefined",
          "resolvedType": "HTMLElement | undefined",
          "required": false,
          "mutable": false,
          "description": "External trigger element to position against. Set via JS: menuEl.anchor = buttonEl"
        },
        "anchorAlignment": {
          "type": "ChoicePopupAnchorAlignment",
          "resolvedType": "\"choice-cell\" | \"popup-frame\"",
          "attribute": "anchor-alignment",
          "default": "'choice-cell'",
          "required": false,
          "mutable": false,
          "description": "Align choice-row edges to the anchor by default; use `popup-frame` only for custom frame geometry."
        },
        "anchorId": {
          "type": "string | undefined",
          "resolvedType": "string | undefined",
          "attribute": "anchor-id",
          "required": false,
          "mutable": false,
          "description": "ID of the external trigger element for positioning"
        },
        "initialFocusVisible": {
          "type": "boolean",
          "resolvedType": "boolean",
          "attribute": "initial-focus-visible",
          "default": "false",
          "required": false,
          "mutable": false,
          "description": "Show a visible ring on the initially focused menu item. Use only when the opener was keyboard-driven."
        },
        "items": {
          "type": "MenuItemData[]",
          "resolvedType": "MenuItemData[]",
          "default": "[]",
          "required": false,
          "mutable": false
        },
        "menuLabel": {
          "type": "string",
          "resolvedType": "string",
          "attribute": "menu-label",
          "default": "'Menu'",
          "required": false,
          "mutable": false,
          "description": "Accessible name for the popup menu."
        },
        "menuWidth": {
          "type": "string | undefined",
          "resolvedType": "string | undefined",
          "attribute": "menu-width",
          "required": false,
          "mutable": false
        },
        "minWidth": {
          "type": "string | undefined",
          "resolvedType": "string | undefined",
          "attribute": "min-width",
          "required": false,
          "mutable": false
        },
        "open": {
          "type": "boolean",
          "resolvedType": "boolean",
          "attribute": "open",
          "default": "false",
          "required": false,
          "mutable": true
        },
        "sections": {
          "type": "MenuSection[]",
          "resolvedType": "MenuSection[]",
          "default": "[]",
          "required": false,
          "mutable": false
        },
        "selectionMode": {
          "type": "MenuSelectionMode",
          "resolvedType": "\"none\" | \"single\"",
          "attribute": "selection-mode",
          "default": "'none'",
          "required": false,
          "mutable": false,
          "description": "Give ordinary rows mutually-exclusive menu semantics using isSelected."
        },
        "side": {
          "type": "MenuSide",
          "resolvedType": "\"bottom\" | \"left\" | \"right\" | \"top\"",
          "attribute": "side",
          "default": "'bottom'",
          "required": false,
          "mutable": false,
          "description": "Preferred side; placement flips to the opposite side when that offers a better fit."
        },
        "sideOffset": {
          "type": "number | string",
          "resolvedType": "number | string",
          "attribute": "side-offset",
          "default": "TOKEN_CSS_LENGTHS.space050",
          "required": false,
          "mutable": false,
          "description": "Gap between anchor and menu — number (px) or TokoMo length (`var(--dimension-space-050)`, etc.)."
        },
        "size": {
          "type": "MenuSize",
          "resolvedType": "\"lg\" | \"md\" | \"sm\" | \"xs\"",
          "attribute": "size",
          "default": "'md'",
          "required": false,
          "mutable": false,
          "description": "Choice-row density."
        }
      },
      "events": [
        {
          "name": "dsAfterClose",
          "detail": "void",
          "bubbles": true,
          "cancelable": true,
          "composed": true,
          "description": "Emitted after the popup's exit motion is complete and its rendered content is removed."
        },
        {
          "name": "dsClose",
          "detail": "void",
          "bubbles": true,
          "cancelable": true,
          "composed": true
        },
        {
          "name": "dsReorder",
          "detail": "MenuReorderDetail",
          "bubbles": true,
          "cancelable": true,
          "composed": true,
          "description": "Emitted after a pointer drop or keyboard move; Menu never mutates item order."
        },
        {
          "name": "dsSelect",
          "detail": "MenuItemData",
          "bubbles": true,
          "cancelable": true,
          "composed": true
        },
        {
          "name": "dsSwatchSelect",
          "detail": "string",
          "bubbles": true,
          "cancelable": true,
          "composed": true,
          "description": "Emitted when a generic `swatch-picker` section option is chosen."
        }
      ],
      "methods": [],
      "slots": []
    },
    "props": {
      "align": {
        "type": "MenuAlign",
        "resolvedType": "\"center\" | \"end\" | \"start\"",
        "attribute": "align",
        "default": "'start'",
        "required": false,
        "mutable": false
      },
      "alignOffset": {
        "type": "number | string",
        "resolvedType": "number | string",
        "attribute": "align-offset",
        "default": "0",
        "required": false,
        "mutable": false,
        "description": "Cross-axis offset — number (px) or TokoMo length."
      },
      "anchor": {
        "type": "HTMLElement | undefined",
        "resolvedType": "HTMLElement | undefined",
        "required": false,
        "mutable": false,
        "description": "External trigger element to position against. Set via JS: menuEl.anchor = buttonEl"
      },
      "anchorAlignment": {
        "type": "ChoicePopupAnchorAlignment",
        "resolvedType": "\"choice-cell\" | \"popup-frame\"",
        "attribute": "anchor-alignment",
        "default": "'choice-cell'",
        "required": false,
        "mutable": false,
        "description": "Align choice-row edges to the anchor by default; use `popup-frame` only for custom frame geometry."
      },
      "anchorId": {
        "type": "string | undefined",
        "resolvedType": "string | undefined",
        "attribute": "anchor-id",
        "required": false,
        "mutable": false,
        "description": "ID of the external trigger element for positioning"
      },
      "initialFocusVisible": {
        "type": "boolean",
        "resolvedType": "boolean",
        "attribute": "initial-focus-visible",
        "default": "false",
        "required": false,
        "mutable": false,
        "description": "Show a visible ring on the initially focused menu item. Use only when the opener was keyboard-driven."
      },
      "items": {
        "type": "MenuItemData[]",
        "resolvedType": "MenuItemData[]",
        "default": "[]",
        "required": false,
        "mutable": false
      },
      "menuLabel": {
        "type": "string",
        "resolvedType": "string",
        "attribute": "menu-label",
        "default": "'Menu'",
        "required": false,
        "mutable": false,
        "description": "Accessible name for the popup menu."
      },
      "menuWidth": {
        "type": "string | undefined",
        "resolvedType": "string | undefined",
        "attribute": "menu-width",
        "required": false,
        "mutable": false
      },
      "minWidth": {
        "type": "string | undefined",
        "resolvedType": "string | undefined",
        "attribute": "min-width",
        "required": false,
        "mutable": false
      },
      "open": {
        "type": "boolean",
        "resolvedType": "boolean",
        "attribute": "open",
        "default": "false",
        "required": false,
        "mutable": true
      },
      "sections": {
        "type": "MenuSection[]",
        "resolvedType": "MenuSection[]",
        "default": "[]",
        "required": false,
        "mutable": false
      },
      "selectionMode": {
        "type": "MenuSelectionMode",
        "resolvedType": "\"none\" | \"single\"",
        "attribute": "selection-mode",
        "default": "'none'",
        "required": false,
        "mutable": false,
        "description": "Give ordinary rows mutually-exclusive menu semantics using isSelected."
      },
      "side": {
        "type": "MenuSide",
        "resolvedType": "\"bottom\" | \"left\" | \"right\" | \"top\"",
        "attribute": "side",
        "default": "'bottom'",
        "required": false,
        "mutable": false,
        "description": "Preferred side; placement flips to the opposite side when that offers a better fit."
      },
      "sideOffset": {
        "type": "number | string",
        "resolvedType": "number | string",
        "attribute": "side-offset",
        "default": "TOKEN_CSS_LENGTHS.space050",
        "required": false,
        "mutable": false,
        "description": "Gap between anchor and menu — number (px) or TokoMo length (`var(--dimension-space-050)`, etc.)."
      },
      "size": {
        "type": "MenuSize",
        "resolvedType": "\"lg\" | \"md\" | \"sm\" | \"xs\"",
        "attribute": "size",
        "default": "'md'",
        "required": false,
        "mutable": false,
        "description": "Choice-row density."
      }
    },
    "events": [
      {
        "name": "dsAfterClose",
        "detail": "void",
        "bubbles": true,
        "cancelable": true,
        "composed": true,
        "description": "Emitted after the popup's exit motion is complete and its rendered content is removed."
      },
      {
        "name": "dsClose",
        "detail": "void",
        "bubbles": true,
        "cancelable": true,
        "composed": true
      },
      {
        "name": "dsReorder",
        "detail": "MenuReorderDetail",
        "bubbles": true,
        "cancelable": true,
        "composed": true,
        "description": "Emitted after a pointer drop or keyboard move; Menu never mutates item order."
      },
      {
        "name": "dsSelect",
        "detail": "MenuItemData",
        "bubbles": true,
        "cancelable": true,
        "composed": true
      },
      {
        "name": "dsSwatchSelect",
        "detail": "string",
        "bubbles": true,
        "cancelable": true,
        "composed": true,
        "description": "Emitted when a generic `swatch-picker` section option is chosen."
      }
    ],
    "methods": [],
    "slots": [],
    "exports": {
      "customElement": "ds-menu",
      "react": "DsMenu",
      "vue": "DsMenu",
      "angular": "DsMenu"
    },
    "consumption": {
      "install": "npm install @ds-mo/ui @ds-mo/tokens @ds-mo/icons",
      "cssSetup": "import '@ds-mo/tokens';\nimport '@ds-mo/tokens/reset';\nimport '@ds-mo/tokens/globals';",
      "customElements": {
        "import": "import '@ds-mo/ui/dist/components/ds-menu.js';",
        "example": "<ds-menu></ds-menu>"
      },
      "react": {
        "import": "import { DsMenu } from '@ds-mo/ui/react';",
        "example": "<DsMenu />"
      },
      "vue": {
        "import": "import { DsMenu } from '@ds-mo/ui/vue';",
        "example": "<DsMenu />"
      },
      "angular": {
        "import": "import { DsMenu } from '@ds-mo/ui/angular/ds-menu';",
        "example": "<ds-menu></ds-menu>"
      },
      "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: anchor, items, sections.",
      "peerDependencies": {
        "required": [
          "@ds-mo/tokens ^6.5.0",
          "@ds-mo/icons ^7.0.0"
        ],
        "frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
      }
    }
  },
  "dependencies": [
    "@ds-mo/ui",
    "@ds-mo/tokens",
    "@ds-mo/icons"
  ],
  "registryDependencies": [
    "badge",
    "icon",
    "swatch-picker",
    "switch",
    "tag",
    "text"
  ],
  "files": [
    {
      "path": "src/wc/components/Menu/Menu.css",
      "content": "@import '../../utils/choice-list.css';\n@import '../../utils/visually-hidden.css';\n@import '../../utils/forced-colors.css';\n\n/* Inactive: `.ds-control-inactive` (50% opacity). */\n\n.menu-item--destructive .menu-item__label {\n  /* Beat ds-text color-* host classes for destructive rows. */\n  color: var(--color-foreground-bold-negative) !important;\n}\n\n/* Standard md icon zone keeps the dot aligned with other control suffixes. */\n.menu-item__dot-box {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  flex-shrink: 0;\n  width: var(--ds-control-icon, var(--dimension-iconography-md));\n  height: var(--ds-control-icon, var(--dimension-iconography-md));\n  pointer-events: none;\n}\n\n/* Switch suffix keeps an 8px label-to-control gap without changing the\n   density gap between a prefix icon or drag handle and the label. */\n.menu-item__switch {\n  flex-shrink: 0;\n  margin-inline-start: calc(\n    var(--dimension-space-100) - var(--ds-control-gap, var(--dimension-space-050))\n  );\n}\n\n/* ── Prefix icon / reorder handle ────────────────────────────────────────── */\n\n.menu-item > .menu-item__handle {\n  pointer-events: auto;\n  cursor: grab;\n  touch-action: none;\n}\n\n.menu-item > .menu-item__handle:active,\n.menu-item--dragging > .menu-item__handle {\n  cursor: grabbing;\n}\n\n.menu-item--dragging {\n  opacity: var(--effect-opacity-medium);\n}\n\n.menu-item__drop-rail {\n  position: absolute;\n  z-index: var(--dimension-z-index-raised);\n  inset-inline: var(--dimension-space-050);\n  block-size: var(--dimension-stroke-width-025);\n  border-radius: var(--dimension-radius-half);\n  background: var(--color-border-bold-brand);\n  pointer-events: none;\n}\n\n.menu-item__drop-rail--before {\n  inset-block-start: calc(-1 * var(--dimension-space-025));\n  transform: translateY(-50%);\n}\n\n.menu-item__drop-rail--after {\n  inset-block-end: calc(-1 * var(--dimension-space-025));\n  transform: translateY(50%);\n}\n\n@media (forced-colors: active) {\n  .menu-item__drop-rail {\n    background: var(--ds-forced-color-selected);\n  }\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "src/wc/components/Menu/Menu.tsx",
      "content": "import {\n  Component,\n  Prop,\n  State,\n  Event,\n  EventEmitter,\n  Element,\n  Watch,\n  Listen,\n  h,\n  Host,\n} from '@stencil/core';\nimport {\n  choicePopupMinWidth,\n  CONTROL_SUPPORTING_TEXT_VARIANT,\n  CONTROL_TEXT_VARIANT,\n  resolveChoicePopupAlignOffset,\n  resolveCssLengthPx,\n  resolveMotionTimeMs,\n  TOKEN_CSS_LENGTHS,\n  TOKEN_DEFAULTS,\n  type ChoicePopupAnchorAlignment,\n  type ControlSize,\n} from '../../utils';\nimport { AnchoredPositionController } from '../../utils/anchored-position-controller';\nimport type { MenuAlign, MenuSide } from './menu-position';\nimport {\n  isMenuPickerSection,\n  isMenuSwatchPickerSection,\n  type MenuItemData,\n  type MenuReorderDetail,\n  type MenuSection,\n} from './menu-types';\nimport { snapshotMenuSections } from './menu-sections';\nimport {\n  createMenuReorderDetail,\n  locateMenuItem,\n  menuReorderableRange,\n  moveReorderableMenuItemBefore,\n  moveReorderableMenuItemBy,\n} from './menu-reorder';\n\nexport type MenuSelectionMode = 'none' | 'single';\nexport type MenuSize = ControlSize;\nconst MENU_ITEM_TAG_SIZE: Record<MenuSize, 'md' | 'sm' | 'xs'> = {\n  lg: 'md',\n  md: 'sm',\n  sm: 'xs',\n  xs: 'xs',\n};\n\nconst MENU_FOCUSABLE_SELECTOR = [\n  'a[href]',\n  'button:not([disabled])',\n  'input:not([disabled]):not([type=\"hidden\"])',\n  'select:not([disabled])',\n  'textarea:not([disabled])',\n  '[tabindex]:not([tabindex=\"-1\"])',\n].join(',');\n\n@Component({\n  tag: 'ds-menu',\n  styleUrl: 'Menu.css',\n  scoped: true,\n})\nexport class Menu {\n  @Element() el!: HTMLElement;\n\n  @Prop({ mutable: true }) open: boolean = false;\n  @Prop() items: MenuItemData[] = [];\n  /** Choice-row density. */\n  @Prop() size: MenuSize = 'md';\n  /** Give ordinary rows mutually-exclusive menu semantics using isSelected. */\n  @Prop() selectionMode: MenuSelectionMode = 'none';\n  @Prop() sections: MenuSection[] = [];\n  /** Preferred side; placement flips to the opposite side when that offers a better fit. */\n  @Prop() side: MenuSide = 'bottom';\n  @Prop() align: MenuAlign = 'start';\n  /** Align choice-row edges to the anchor by default; use `popup-frame` only for custom frame geometry. */\n  @Prop() anchorAlignment: ChoicePopupAnchorAlignment = 'choice-cell';\n  /** Gap between anchor and menu — number (px) or TokoMo length (`var(--dimension-space-050)`, etc.). */\n  @Prop() sideOffset: number | string = TOKEN_CSS_LENGTHS.space050;\n  /** Cross-axis offset — number (px) or TokoMo length. */\n  @Prop() alignOffset: number | string = 0;\n  @Prop() menuWidth: string | undefined;\n  @Prop() minWidth: string | undefined;\n  /** External trigger element to position against. Set via JS: menuEl.anchor = buttonEl */\n  @Prop() anchor: HTMLElement | undefined;\n  /** ID of the external trigger element for positioning */\n  @Prop() anchorId: string | undefined;\n  /** Show a visible ring on the initially focused menu item. Use only when the opener was keyboard-driven. */\n  @Prop() initialFocusVisible: boolean = false;\n  /** Accessible name for the popup menu. */\n  @Prop() menuLabel: string = 'Menu';\n\n  @State() private shouldRender: boolean = false;\n  @State() private closing: boolean = false;\n  @State() private pos: { x: number; y: number } = { x: 0, y: 0 };\n  @State() private focusedIndex: number = 0;\n  @State() private positionReady: boolean = false;\n  @State() private focusRingVisible: boolean = false;\n  @State() private reorderFromFlat: number | null = null;\n  @State() private reorderInsertBefore: number | null = null;\n  @State() private reorderAnnouncement = '';\n\n  @Event() dsClose!: EventEmitter<void>;\n  /** Emitted after the popup's exit motion is complete and its rendered content is removed. */\n  @Event() dsAfterClose!: EventEmitter<void>;\n  @Event() dsSelect!: EventEmitter<MenuItemData>;\n  /** Emitted when a generic `swatch-picker` section option is chosen. */\n  @Event() dsSwatchSelect!: EventEmitter<string>;\n  /** Emitted after a pointer drop or keyboard move; Menu never mutates item order. */\n  @Event() dsReorder!: EventEmitter<MenuReorderDetail>;\n\n  private clickOutsideHandler: ((e: MouseEvent) => void) | null = null;\n  private closeTimer: ReturnType<typeof setTimeout> | null = null;\n  private positionReadyCallback: (() => void) | undefined;\n  private suppressItemClick = false;\n  private reorderPointer: {\n    pointerId: number;\n    sectionIndex: number;\n    fromIndex: number;\n  } | null = null;\n  /**\n   * Anchored placement. The choice-cell measurement stays here on purpose: the\n   * inner-cell align offset and the `--dimension-menu-width-xs` floor are Menu\n   * semantics, not shared geometry.\n   */\n  private readonly position = new AnchoredPositionController({\n    getAnchor: () => this.resolvedAnchor ?? null,\n    getPopup: () => this.el.querySelector<HTMLElement>('.menu-popup'),\n    measure: (anchorEl, popup) => {\n      if (!this.open) return null;\n\n      const anchorRect = anchorEl.getBoundingClientRect();\n      const sectionInsetPx = this.viewportPadPx;\n      if (!this.minWidth) {\n        if (\n          this.anchorAlignment === 'choice-cell' &&\n          (this.side === 'top' || this.side === 'bottom')\n        ) {\n          popup.style.minWidth = `max(var(--dimension-menu-width-xs), ${choicePopupMinWidth(\n            anchorRect.width,\n            sectionInsetPx\n          )}px)`;\n        } else {\n          popup.style.removeProperty('min-width');\n        }\n      }\n\n      return {\n        anchorRect,\n        popupWidth: popup.offsetWidth || this.popupFallbackWidthPx,\n        popupHeight: popup.offsetHeight || this.popupFallbackHeightPx,\n        side: this.side,\n        align: this.align,\n        sideOffsetPx: this.sideOffsetPx,\n        alignOffsetPx: resolveChoicePopupAlignOffset({\n          align: this.align,\n          alignOffsetPx: this.alignOffsetPx,\n          sectionInsetPx,\n          anchorAlignment: this.anchorAlignment,\n        }),\n        viewportPadPx: this.viewportPadPx,\n        viewportWidth: window.innerWidth,\n        viewportHeight: window.innerHeight,\n      };\n    },\n    apply: ({ x, y }) => {\n      this.pos = { x, y };\n    },\n    onReady: () => {\n      this.positionReady = true;\n    },\n    liveUpdate: 'double-frame',\n    topLayer: true,\n  });\n  /** Last content actually painted while open; retained unchanged through exit motion. */\n  private lastRenderedSections: MenuSection[] = [];\n  private closingSections: MenuSection[] | null = null;\n\n  componentDidLoad() {\n    if (this.open) this.onOpenChange(true);\n  }\n\n  disconnectedCallback() {\n    this.cancelPositionRetry();\n    this.cancelLivePositionUpdate();\n    this.clearReorderPointer();\n    this.teardownListeners();\n  }\n\n  @Watch('open')\n  onOpenChange(isOpen: boolean) {\n    if (isOpen) {\n      this.teardownListeners();\n      this.closingSections = null;\n      this.shouldRender = true;\n      this.closing = false;\n      this.positionReady = false;\n      this.focusRingVisible = this.initialFocusVisible;\n      this.setupListeners();\n      this.schedulePositionUpdate(() => {\n        this.focusInitialItem();\n      });\n    } else if (this.shouldRender) {\n      this.positionReadyCallback = undefined;\n      this.cancelPositionRetry();\n      this.captureClosingSections();\n      this.closing = true;\n      this.teardownListeners();\n      const closeAnimationMs = this.closeAnimationMs;\n      if (closeAnimationMs <= 0) {\n        this.finishClose();\n        return;\n      }\n      this.closeTimer = setTimeout(() => {\n        this.closeTimer = null;\n        this.finishClose();\n      }, closeAnimationMs);\n    }\n  }\n\n  @Watch('anchor')\n  @Watch('anchorId')\n  onAnchorChange() {\n    if (this.open) this.schedulePositionUpdate();\n  }\n\n  @Watch('side')\n  @Watch('align')\n  @Watch('anchorAlignment')\n  @Watch('sideOffset')\n  @Watch('alignOffset')\n  onPositionPropsChange() {\n    if (this.open) this.schedulePositionUpdate();\n  }\n\n  private get viewportPadPx(): number {\n    return resolveCssLengthPx(TOKEN_DEFAULTS.space050, TOKEN_DEFAULTS.space050);\n  }\n\n  private get sideOffsetPx(): number {\n    return resolveCssLengthPx(this.sideOffset, TOKEN_DEFAULTS.space050);\n  }\n\n  private get alignOffsetPx(): number {\n    return resolveCssLengthPx(this.alignOffset, 0);\n  }\n\n  private get popupFallbackWidthPx(): number {\n    return resolveCssLengthPx(TOKEN_DEFAULTS.menuWidthXs, TOKEN_DEFAULTS.menuWidthXs);\n  }\n\n  private get popupFallbackHeightPx(): number {\n    return resolveCssLengthPx(TOKEN_DEFAULTS.menuFallbackHeight, TOKEN_DEFAULTS.menuFallbackHeight);\n  }\n\n  private get closeAnimationMs(): number {\n    return resolveMotionTimeMs(TOKEN_DEFAULTS.motionShort2, TOKEN_DEFAULTS.animationDurationShort3);\n  }\n\n  private finishClose() {\n    const popup = this.el.querySelector<HTMLElement>('.menu-popup');\n    if (popup?.matches(':popover-open')) popup.hidePopover();\n    this.shouldRender = false;\n    this.closing = false;\n    this.lastRenderedSections = [];\n    this.closingSections = null;\n    this.clearReorderPointer();\n    this.reorderAnnouncement = '';\n    this.dsAfterClose.emit();\n  }\n\n  private captureClosingSections() {\n    if (this.closingSections) return;\n    const visibleSections = this.lastRenderedSections.length\n      ? this.lastRenderedSections\n      : this.activeSections;\n    this.closingSections = snapshotMenuSections(visibleSections);\n  }\n\n  private get resolvedAnchor(): HTMLElement | null {\n    if (this.anchor) return this.anchor;\n    if (this.anchorId) return document.getElementById(this.anchorId);\n    return null;\n  }\n\n  private get anchorFocusTarget(): HTMLElement | null {\n    const anchor = this.resolvedAnchor;\n    if (!anchor) return null;\n    if (anchor.matches(MENU_FOCUSABLE_SELECTOR)) return anchor;\n    return anchor.querySelector<HTMLElement>(MENU_FOCUSABLE_SELECTOR) ?? anchor;\n  }\n\n  private focusAnchor() {\n    const anchor = this.resolvedAnchor as\n      (HTMLElement & { setFocus?: () => Promise<void> | void }) | null;\n    if (anchor?.setFocus) {\n      anchor.setFocus();\n      return;\n    }\n    this.anchorFocusTarget?.focus();\n  }\n\n  private moveFocusAfterTab(backwards: boolean) {\n    const anchor = this.anchorFocusTarget;\n    if (!anchor) return;\n    if (backwards) {\n      anchor.focus();\n      return;\n    }\n\n    const candidates = Array.from(\n      document.querySelectorAll<HTMLElement>(MENU_FOCUSABLE_SELECTOR)\n    ).filter(\n      element =>\n        !element.closest('.menu-popup') &&\n        !element.closest('[inert]') &&\n        element.getClientRects().length > 0\n    );\n    const index = candidates.indexOf(anchor);\n    (candidates[index + 1] ?? anchor).focus();\n  }\n\n  private compositeTabLeavesPopup(event: KeyboardEvent): boolean {\n    const popup = this.el.querySelector<HTMLElement>('.menu-popup');\n    if (!popup) return true;\n    const focusables = Array.from(\n      popup.querySelectorAll<HTMLElement>(MENU_FOCUSABLE_SELECTOR)\n    ).filter(element => element.getClientRects().length > 0);\n    const currentIndex = focusables.indexOf(event.target as HTMLElement);\n    if (currentIndex < 0) return true;\n    return event.shiftKey ? currentIndex === 0 : currentIndex === focusables.length - 1;\n  }\n\n  private get activeSections(): MenuSection[] {\n    if (this.sections.length > 0) return this.sections;\n    if (this.items.length > 0) return [{ items: this.items }];\n    return [];\n  }\n\n  private get flatItems(): MenuItemData[] {\n    return this.activeSections.flatMap(section =>\n      isMenuPickerSection(section) ? [] : section.items\n    );\n  }\n\n  /** Rich preference content is a non-modal dialog, not an ARIA action menu. */\n  private get hasCompositeSections(): boolean {\n    return this.activeSections.some(isMenuPickerSection);\n  }\n\n  private cancelPositionRetry() {\n    this.position.cancel();\n  }\n\n  private cancelLivePositionUpdate() {\n    this.position.cancel();\n  }\n\n  /** Reposition after the current scroll/layout frame without hiding the open popup. */\n  private scheduleLivePositionUpdate() {\n    this.position.scheduleLiveUpdate();\n  }\n\n  /** Retry until anchor + popup exist — do not reveal at 0,0 on a failed first pass. */\n  private schedulePositionUpdate(onReady?: () => void) {\n    if (!this.open) return;\n    this.positionReady = false;\n    // Opening often sets `open` and `anchorId` together. Each Watch reschedules\n    // placement; keep the open-path ready callback so initial focus is not dropped.\n    if (onReady) this.positionReadyCallback = onReady;\n    const callback = this.positionReadyCallback;\n    this.position.schedule(() => {\n      if (this.positionReadyCallback === callback) {\n        this.positionReadyCallback = undefined;\n      }\n      callback?.();\n    });\n  }\n\n  /** @returns `true` when anchor and popup were found and `pos` was updated. */\n  private calculatePosition(): boolean {\n    return this.position.update();\n  }\n\n  /** Focus the selected item when present, otherwise the first enabled item. */\n  private focusInitialItem() {\n    if (this.hasCompositeSections) {\n      requestAnimationFrame(() => {\n        this.el.querySelector<HTMLElement>('.menu-popup [tabindex=\"0\"]')?.focus();\n      });\n      return;\n    }\n\n    const flat = this.flatItems;\n    const selectedIdx = flat.findIndex(it => it.isSelected && !it.isInactive);\n    const firstEnabledIdx = flat.findIndex(it => !it.isInactive || !!it.reorderable);\n    this.focusedIndex = selectedIdx >= 0 ? selectedIdx : firstEnabledIdx >= 0 ? firstEnabledIdx : 0;\n\n    requestAnimationFrame(() => {\n      const btns = this.el.querySelectorAll<HTMLElement>('.menu-item');\n      btns[this.focusedIndex]?.focus();\n    });\n  }\n\n  private setupListeners() {\n    this.clickOutsideHandler = (e: MouseEvent) => {\n      const t = e.target as Node;\n      const popup = this.el.querySelector('.menu-popup');\n      const anchorEl = this.resolvedAnchor;\n      if (popup?.contains(t) || anchorEl?.contains(t)) return;\n      this.close();\n    };\n\n    document.addEventListener('mousedown', this.clickOutsideHandler, true);\n    this.position.observe();\n  }\n\n  private teardownListeners() {\n    this.position.unobserve();\n    if (this.clickOutsideHandler) {\n      document.removeEventListener('mousedown', this.clickOutsideHandler, true);\n      this.clickOutsideHandler = null;\n    }\n    if (this.closeTimer) {\n      clearTimeout(this.closeTimer);\n      this.closeTimer = null;\n    }\n  }\n\n  private close(restoreFocus = true) {\n    this.captureClosingSections();\n    if (restoreFocus) this.focusAnchor();\n    this.dsClose.emit();\n    this.open = false;\n    this.onOpenChange(false);\n  }\n\n  @Listen('keydown')\n  handleKeyDown(e: KeyboardEvent) {\n    if (!this.shouldRender || this.closing) return;\n\n    if (e.key === 'Escape') {\n      e.preventDefault();\n      this.close();\n      return;\n    }\n\n    if (e.key === 'Tab') {\n      if (this.hasCompositeSections && !this.compositeTabLeavesPopup(e)) return;\n      e.preventDefault();\n      this.close(false);\n      this.moveFocusAfterTab(e.shiftKey);\n      return;\n    }\n\n    if (this.hasCompositeSections) return;\n\n    if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && e.altKey && !e.ctrlKey && !e.shiftKey) {\n      this.handleReorderKey(e);\n      return;\n    }\n\n    const flat = this.flatItems;\n    const enabled = flat\n      .map((it, i) => ({ it, i }))\n      .filter(({ it }) => !it.isInactive || !!it.reorderable)\n      .map(({ i }) => i);\n    if (!enabled.length) return;\n\n    const cur = enabled.indexOf(this.focusedIndex);\n    const safe = cur < 0 ? 0 : cur;\n\n    switch (e.key) {\n      case 'ArrowDown':\n        e.preventDefault();\n        e.stopPropagation();\n        this.focusRingVisible = true;\n        this.focusedIndex = enabled[(safe + 1) % enabled.length];\n        this.focusItem(this.focusedIndex);\n        break;\n      case 'ArrowUp':\n        e.preventDefault();\n        e.stopPropagation();\n        this.focusRingVisible = true;\n        this.focusedIndex = enabled[(safe - 1 + enabled.length) % enabled.length];\n        this.focusItem(this.focusedIndex);\n        break;\n      case 'Home':\n        e.preventDefault();\n        e.stopPropagation();\n        this.focusRingVisible = true;\n        this.focusedIndex = enabled[0];\n        this.focusItem(this.focusedIndex);\n        break;\n      case 'End':\n        e.preventDefault();\n        e.stopPropagation();\n        this.focusRingVisible = true;\n        this.focusedIndex = enabled[enabled.length - 1];\n        this.focusItem(this.focusedIndex);\n        break;\n    }\n  }\n\n  private focusItem(idx: number) {\n    const btns = this.el.querySelectorAll<HTMLElement>('.menu-item');\n    btns[idx]?.focus();\n  }\n\n  private handleItemClick(item: MenuItemData) {\n    if (this.suppressItemClick) {\n      this.suppressItemClick = false;\n      return;\n    }\n    if (item.isInactive) return;\n    // Consumers commonly change the item source while handling selection.\n    // Capture first so the exit animation never paints the next menu context.\n    this.captureClosingSections();\n    this.dsSelect.emit(item);\n  }\n\n  private handleSwatchSelect(value: string) {\n    this.dsSwatchSelect.emit(value);\n  }\n\n  private handleReorderKey(event: KeyboardEvent): void {\n    const located = locateMenuItem(this.activeSections, this.focusedIndex);\n    if (!located || !located.items[located.itemIndex]?.reorderable) return;\n\n    const nextItems = moveReorderableMenuItemBy(\n      located.items,\n      located.itemIndex,\n      event.key === 'ArrowUp' ? -1 : 1\n    );\n    if (!nextItems) return;\n\n    event.preventDefault();\n    event.stopPropagation();\n    this.emitReorder(located.items, located.itemIndex, nextItems, located.sectionIndex);\n  }\n\n  private emitReorder(\n    items: MenuItemData[],\n    fromIndex: number,\n    nextItems: MenuItemData[],\n    sectionIndex: number\n  ): void {\n    const detail = createMenuReorderDetail(items, fromIndex, nextItems, sectionIndex);\n    if (!detail) return;\n    this.focusedIndex = this.flatIndexFor(sectionIndex, detail.toIndex);\n    this.announceReorder(detail);\n    this.dsReorder.emit(detail);\n  }\n\n  private flatIndexFor(sectionIndex: number, itemIndex: number): number {\n    let flat = 0;\n    for (let index = 0; index < this.activeSections.length; index += 1) {\n      const section = this.activeSections[index];\n      if (isMenuPickerSection(section)) continue;\n      if (index === sectionIndex) return flat + itemIndex;\n      flat += section.items.length;\n    }\n    return itemIndex;\n  }\n\n  private announceReorder(detail: MenuReorderDetail): void {\n    const range = menuReorderableRange(detail.items, detail.toIndex);\n    const count = range ? range.end - range.start + 1 : detail.items.length;\n    const position = range ? detail.toIndex - range.start + 1 : detail.toIndex + 1;\n    this.reorderAnnouncement = `${detail.item.label} moved to position ${position} of ${count}`;\n  }\n\n  private onReorderPointerDown(\n    event: PointerEvent,\n    sectionIndex: number,\n    itemIndex: number,\n    flatIndex: number\n  ): void {\n    if (event.button !== 0) return;\n    const section = this.activeSections[sectionIndex];\n    if (!section || isMenuPickerSection(section)) return;\n    if (!section.items[itemIndex]?.reorderable) return;\n\n    event.preventDefault();\n    event.stopPropagation();\n    this.suppressItemClick = true;\n    this.focusRingVisible = false;\n    this.focusedIndex = flatIndex;\n    this.focusItem(flatIndex);\n    this.reorderPointer = {\n      pointerId: event.pointerId,\n      sectionIndex,\n      fromIndex: itemIndex,\n    };\n    this.reorderFromFlat = flatIndex;\n    this.reorderInsertBefore = itemIndex;\n    window.addEventListener('pointermove', this.onReorderPointerMove);\n    window.addEventListener('pointerup', this.onReorderPointerUp);\n    window.addEventListener('pointercancel', this.onReorderPointerCancel);\n  }\n\n  private onReorderPointerMove = (event: PointerEvent) => {\n    const drag = this.reorderPointer;\n    if (!drag || event.pointerId !== drag.pointerId) return;\n    const section = this.activeSections[drag.sectionIndex];\n    if (!section || isMenuPickerSection(section)) return;\n\n    const insertBefore = this.resolveReorderInsertBefore(\n      event.clientY,\n      drag.sectionIndex,\n      drag.fromIndex,\n      section.items\n    );\n    if (insertBefore !== this.reorderInsertBefore) this.reorderInsertBefore = insertBefore;\n  };\n\n  private onReorderPointerUp = (event: PointerEvent) => {\n    const drag = this.reorderPointer;\n    if (!drag || event.pointerId !== drag.pointerId) return;\n\n    const section = this.activeSections[drag.sectionIndex];\n    const insertBefore = this.reorderInsertBefore;\n    this.clearReorderPointer();\n    queueMicrotask(() => {\n      this.suppressItemClick = false;\n    });\n    if (!section || isMenuPickerSection(section) || insertBefore === null) return;\n\n    const nextItems = moveReorderableMenuItemBefore(section.items, drag.fromIndex, insertBefore);\n    if (!nextItems) return;\n    this.emitReorder(section.items, drag.fromIndex, nextItems, drag.sectionIndex);\n  };\n\n  private onReorderPointerCancel = (event: PointerEvent) => {\n    if (!this.reorderPointer || event.pointerId !== this.reorderPointer.pointerId) return;\n    this.clearReorderPointer();\n    queueMicrotask(() => {\n      this.suppressItemClick = false;\n    });\n  };\n\n  private resolveReorderInsertBefore(\n    clientY: number,\n    sectionIndex: number,\n    fromIndex: number,\n    items: MenuItemData[]\n  ): number {\n    const range = menuReorderableRange(items, fromIndex);\n    if (!range) return fromIndex;\n\n    const buttons = [...this.el.querySelectorAll<HTMLElement>('.menu-item')];\n    const sectionStart = this.flatIndexFor(sectionIndex, 0);\n    let insertBefore = range.end + 1;\n    for (let itemIndex = range.start; itemIndex <= range.end; itemIndex += 1) {\n      const row = buttons[sectionStart + itemIndex];\n      if (!row) continue;\n      const rect = row.getBoundingClientRect();\n      if (clientY < rect.top + rect.height / 2) {\n        insertBefore = itemIndex;\n        break;\n      }\n    }\n    return Math.min(Math.max(insertBefore, range.start), range.end + 1);\n  }\n\n  private clearReorderPointer(): void {\n    if (this.reorderPointer) {\n      window.removeEventListener('pointermove', this.onReorderPointerMove);\n      window.removeEventListener('pointerup', this.onReorderPointerUp);\n      window.removeEventListener('pointercancel', this.onReorderPointerCancel);\n    }\n    this.reorderPointer = null;\n    this.reorderFromFlat = null;\n    this.reorderInsertBefore = null;\n  }\n\n  render() {\n    if (!this.shouldRender) return <Host style={{ display: 'contents' }} />;\n\n    const sections = this.closing\n      ? (this.closingSections ?? this.lastRenderedSections)\n      : this.activeSections;\n    const hasCompositeSections = sections.some(isMenuPickerSection);\n    if (!this.closing) {\n      this.lastRenderedSections = snapshotMenuSections(sections);\n      this.closingSections = null;\n    }\n    let flatIdx = 0;\n    const dragLocated =\n      this.reorderFromFlat === null ? null : locateMenuItem(sections, this.reorderFromFlat);\n    const dragRange = dragLocated\n      ? menuReorderableRange(dragLocated.items, dragLocated.itemIndex)\n      : null;\n\n    const popupStyle: Record<string, string> = {\n      position: 'fixed',\n      left: '0',\n      top: '0',\n      transform: `translate(${Math.round(this.pos.x)}px, ${Math.round(this.pos.y)}px)`,\n      zIndex: '9998',\n      visibility: this.positionReady ? 'visible' : 'hidden',\n    };\n\n    if (this.menuWidth) popupStyle['width'] = this.menuWidth;\n    if (this.minWidth) popupStyle['min-width'] = this.minWidth;\n\n    return (\n      <Host style={{ display: 'contents' }}>\n        <div\n          popover=\"manual\"\n          class={{\n            'menu-popup': true,\n            'menu-popup--closing': this.closing,\n            'ds-choice-popup': true,\n            'ds-choice-popup--closing': this.closing,\n          }}\n          style={popupStyle}\n          role={hasCompositeSections ? 'dialog' : 'menu'}\n          aria-label={this.menuLabel}\n          aria-orientation={hasCompositeSections ? undefined : 'vertical'}\n        >\n          <div class=\"ds-choice-list\">\n            {sections.map((section, si) => (\n              <div\n                key={si}\n                class={{\n                  'menu-section': true,\n                  'menu-section--divided': si < sections.length - 1,\n                  'menu-section--swatch-picker': isMenuPickerSection(section),\n                  'ds-choice-section': true,\n                  'ds-chrome-column': true,\n                  'ds-chrome-space--sm': true,\n                  'ds-choice-section--divided': si < sections.length - 1,\n                  'ds-choice-section--headed-after-first': si > 0 && Boolean(section.header),\n                }}\n                role={section.header ? 'group' : undefined}\n                aria-label={section.header}\n              >\n                {section.header && (\n                  <ds-text\n                    class={`section-header ds-choice-section__header ds-control-section-heading ds-control--${this.size}`}\n                    as=\"span\"\n                    variant={CONTROL_SUPPORTING_TEXT_VARIANT[this.size]}\n                    emphasis\n                    color=\"primary\"\n                    aria-hidden=\"true\"\n                  >\n                    <span class=\"ds-choice-section__header-label\">{section.header}</span>\n                  </ds-text>\n                )}\n                {isMenuSwatchPickerSection(section) ? (\n                  <ds-swatch-picker\n                    value={section.value}\n                    groupLabel={section.groupLabel ?? section.header ?? 'Swatch options'}\n                    options={section.options ?? []}\n                    sections={section.sections ?? []}\n                    onDsChange={(e: CustomEvent<string>) => {\n                      e.stopPropagation();\n                      this.handleSwatchSelect(e.detail);\n                    }}\n                  />\n                ) : (\n                  section.items.map((item, itemIndex) => {\n                    const idx = flatIdx++;\n                    const isFocused = this.focusedIndex === idx;\n                    const isSingleSelectionItem =\n                      !hasCompositeSections && this.selectionMode === 'single' && !item.showSwitch;\n                    const usesLeading = section.items.some(\n                      candidate => candidate.reorderable || !!candidate.icon\n                    );\n                    const locked = !!item.isInactive && !item.reorderable;\n                    const dragging = this.reorderFromFlat === idx;\n                    const dropBefore =\n                      dragLocated?.sectionIndex === si &&\n                      dragRange !== null &&\n                      itemIndex <= dragRange.end &&\n                      this.reorderInsertBefore === itemIndex;\n                    const dropAfter =\n                      dragLocated?.sectionIndex === si &&\n                      dragRange !== null &&\n                      this.reorderInsertBefore === dragRange.end + 1 &&\n                      itemIndex === dragRange.end;\n                    return (\n                      <button\n                        key={item.value ?? `${si}-${item.label}`}\n                        type=\"button\"\n                        class={{\n                          'menu-item': true,\n                          'ds-choice-item': true,\n                          'ds-control-frame': true,\n                          [`ds-control--${this.size}`]: true,\n                          'ds-focus-ring-inset': true,\n                          'ds-focus-ring--visible': isFocused && this.focusRingVisible,\n                          'ds-interaction-fill': !locked,\n                          'ds-interaction-fill--selected': !!item.isSelected && !locked,\n                          'menu-item--selected': !!item.isSelected,\n                          'menu-item--switch': !!item.showSwitch,\n                          'ds-control-inactive': locked,\n                          'menu-item--destructive': !!item.isDestructive,\n                          'menu-item--focused': isFocused,\n                          'menu-item--dragging': dragging,\n                        }}\n                        role={\n                          hasCompositeSections\n                            ? undefined\n                            : item.showSwitch\n                              ? 'menuitemcheckbox'\n                              : isSingleSelectionItem\n                                ? 'menuitemradio'\n                                : 'menuitem'\n                        }\n                        aria-checked={\n                          !hasCompositeSections && item.showSwitch\n                            ? String(!!item.switchValue)\n                            : isSingleSelectionItem\n                              ? String(!!item.isSelected)\n                              : undefined\n                        }\n                        aria-pressed={\n                          hasCompositeSections\n                            ? String(item.showSwitch ? !!item.switchValue : !!item.isSelected)\n                            : undefined\n                        }\n                        aria-current={\n                          !hasCompositeSections &&\n                          !item.showSwitch &&\n                          !isSingleSelectionItem &&\n                          item.isSelected\n                            ? 'true'\n                            : undefined\n                        }\n                        aria-description={\n                          item.reorderable\n                            ? item.isInactive\n                              ? 'Visibility cannot be changed. Alt + Arrow Up or Alt + Arrow Down moves this row.'\n                              : 'Drag to reorder. Alt + Arrow Up or Alt + Arrow Down moves this row.'\n                            : undefined\n                        }\n                        aria-disabled={item.isInactive ? 'true' : undefined}\n                        disabled={locked}\n                        tabIndex={hasCompositeSections ? 0 : isFocused ? 0 : -1}\n                        onMouseDown={() => {\n                          this.focusRingVisible = false;\n                        }}\n                        onClick={() => this.handleItemClick(item)}\n                        onFocus={() => {\n                          this.focusedIndex = idx;\n                        }}\n                      >\n                        {(dropBefore || dropAfter) && (\n                          <span\n                            class={{\n                              'menu-item__drop-rail': true,\n                              'menu-item__drop-rail--before': dropBefore,\n                              'menu-item__drop-rail--after': dropAfter,\n                            }}\n                            data-menu-drop-rail\n                            aria-hidden=\"true\"\n                          />\n                        )}\n                        {item.reorderable ? (\n                          <span\n                            class=\"menu-item__handle ds-choice-item__icon ds-control-icon-box ds-interaction-fill__content\"\n                            data-menu-handle\n                            aria-hidden=\"true\"\n                            onPointerDown={event =>\n                              this.onReorderPointerDown(event, si, itemIndex, idx)\n                            }\n                            onClick={event => {\n                              event.preventDefault();\n                              event.stopPropagation();\n                            }}\n                          >\n                            <ds-icon name=\"Drag\" size={this.size} color=\"inherit\" />\n                          </span>\n                        ) : usesLeading && item.icon ? (\n                          <span\n                            class=\"menu-item__icon ds-choice-item__icon ds-control-icon-box ds-interaction-fill__content\"\n                            aria-hidden=\"true\"\n                          >\n                            <ds-icon name={item.icon} size={this.size} color=\"inherit\" />\n                          </span>\n                        ) : usesLeading ? (\n                          <span\n                            class=\"menu-item__icon-spacer ds-choice-item__icon ds-control-icon-box\"\n                            aria-hidden=\"true\"\n                          />\n                        ) : null}\n                        <div class=\"menu-item__content ds-choice-item__content ds-interaction-fill__content\">\n                          <ds-text\n                            class=\"menu-item__label ds-choice-item__label ds-control-label-box\"\n                            as=\"span\"\n                            variant={CONTROL_TEXT_VARIANT[this.size]}\n                            color={item.isSelected ? 'primary' : 'secondary'}\n                          >\n                            {item.label}\n                          </ds-text>\n                          {item.subtext && (\n                            <ds-text\n                              class=\"menu-item__subtext ds-choice-item__subtext ds-control-label-box\"\n                              as=\"span\"\n                              variant={CONTROL_SUPPORTING_TEXT_VARIANT[this.size]}\n                              color=\"secondary\"\n                            >\n                              {item.subtext}\n                            </ds-text>\n                          )}\n                        </div>\n                        {item.tag && (\n                          <ds-tag\n                            class=\"ds-choice-item__tag ds-interaction-fill__content\"\n                            label={item.tag.label}\n                            size={MENU_ITEM_TAG_SIZE[this.size]}\n                            intent={item.tag.intent ?? 'neutral'}\n                            contrast={item.tag.contrast ?? 'faint'}\n                            rounded={item.tag.rounded ?? false}\n                            isInset\n                          />\n                        )}\n                        {item.dot && (\n                          <span\n                            class=\"menu-item__dot-box ds-interaction-fill__content\"\n                            aria-hidden=\"true\"\n                          >\n                            <ds-badge\n                              class=\"menu-item__dot\"\n                              variant=\"dot\"\n                              hasRing={false}\n                              label=\"\"\n                            />\n                          </span>\n                        )}\n                        {item.showSwitch && (\n                          <ds-switch\n                            class=\"menu-item__switch ds-interaction-fill__content\"\n                            size={this.size}\n                            checked={!!item.switchValue}\n                            isInactive={!!item.isInactive}\n                            presentation\n                          />\n                        )}\n                      </button>\n                    );\n                  })\n                )}\n              </div>\n            ))}\n          </div>\n        </div>\n        <div class=\"ds-visually-hidden\" role=\"status\" aria-live=\"polite\">\n          {this.reorderAnnouncement}\n        </div>\n      </Host>\n    );\n  }\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "src/wc/components/Menu/menu-placement.ts",
      "content": "import type { ChoicePopupAnchorAlignment } from '../../utils/choice-popup-alignment';\nimport type { MenuAlign, MenuSide } from './menu-position';\n\n/** Placement props bindable on `ds-menu` (side, align, offsets). */\nexport interface MenuPlacement {\n  side: MenuSide;\n  align: MenuAlign;\n  anchorAlignment: ChoicePopupAnchorAlignment;\n  sideOffset: number | string;\n  alignOffset: number | string;\n}\n\n/**\n * Canonical `ds-menu` placement for the **panel-nav footer user menu**.\n * Anchor from `dsNavUserAction` detail `{ anchor }` (id `ds-panel-nav-user-menu-anchor`).\n *\n * Not for BarNav overflow menus — those use `side=\"bottom\"` / `align=\"end\"` internally.\n * `align: 'end'` bottom-aligns the last choice cell on the anchor; the side offset nudges into the content area:\n * - `sideOffset` space-100 + space-050 (12px): 4px past the panel-nav outer edge (anchor is ~8px inset).\n * - `anchorAlignment: 'choice-cell'`: the shared section inset puts the last row edge on the footer user row.\n */\nexport const PANEL_NAV_USER_MENU_PLACEMENT = {\n  side: 'right',\n  align: 'end',\n  anchorAlignment: 'choice-cell',\n  sideOffset: 'calc(var(--dimension-space-100) + var(--dimension-space-050))',\n  alignOffset: 0,\n} as const satisfies MenuPlacement;\n\n/**\n * Canonical placement for a menu opened by a PanelTools header action.\n * Matches BarNav overflow: the 32px trigger is 8px inside the 48px header, so\n * a 12px side offset places the popup 4px below the header edge. Choice-cell\n * alignment keeps the final menu item edge aligned with the trigger.\n */\nexport const PANEL_TOOLS_HEADER_MENU_PLACEMENT = {\n  side: 'bottom',\n  align: 'end',\n  anchorAlignment: 'choice-cell',\n  sideOffset: 'calc(var(--dimension-space-100) + var(--dimension-space-050))',\n  alignOffset: 0,\n} as const satisfies MenuPlacement;\n",
      "type": "registry:ui"
    },
    {
      "path": "src/wc/components/Menu/menu-position.ts",
      "content": "import {\n  computeAnchoredPosition,\n  type AnchoredAlign,\n  type AnchoredPositionInput,\n  type AnchoredSide,\n} from '../../utils/anchored-position';\n\nexport type MenuSide = AnchoredSide;\nexport type MenuAlign = AnchoredAlign;\nexport type MenuPositionInput = AnchoredPositionInput;\n\n/** Backward-compatible menu alias for the shared anchored-position layout math. */\nexport const computeMenuPosition = computeAnchoredPosition;\n",
      "type": "registry:ui"
    },
    {
      "path": "src/wc/components/Menu/menu-reorder.ts",
      "content": "import {\n  isMenuPickerSection,\n  type MenuItemData,\n  type MenuReorderDetail,\n  type MenuSection,\n} from './menu-types';\n\nexport interface MenuReorderRange {\n  start: number;\n  end: number;\n}\n\n/** Contiguous reorderable run containing `index`, or null when that row cannot move. */\nexport function menuReorderableRange(\n  items: readonly MenuItemData[],\n  index: number,\n): MenuReorderRange | null {\n  if (!items[index]?.reorderable) return null;\n\n  let start = index;\n  while (start > 0 && items[start - 1]?.reorderable) start -= 1;\n\n  let end = index;\n  while (end < items.length - 1 && items[end + 1]?.reorderable) end += 1;\n\n  return { start, end };\n}\n\n/**\n * Move a reorderable row so it would be inserted before `insertBefore`.\n * `insertBefore` is clamped to the contiguous reorderable run.\n */\nexport function moveReorderableMenuItemBefore(\n  items: readonly MenuItemData[],\n  fromIndex: number,\n  insertBefore: number,\n): MenuItemData[] | null {\n  const range = menuReorderableRange(items, fromIndex);\n  if (!range) return null;\n\n  const clamped = Math.min(Math.max(insertBefore, range.start), range.end + 1);\n  let toIndex = clamped;\n  if (fromIndex < toIndex) toIndex -= 1;\n  if (toIndex === fromIndex || toIndex < range.start || toIndex > range.end) return null;\n\n  const next = [...items];\n  const [item] = next.splice(fromIndex, 1);\n  next.splice(toIndex, 0, item);\n  return next;\n}\n\n/** Keyboard step: move one slot within the reorderable run. */\nexport function moveReorderableMenuItemBy(\n  items: readonly MenuItemData[],\n  fromIndex: number,\n  offset: number,\n): MenuItemData[] | null {\n  if (offset === 0) return null;\n  return moveReorderableMenuItemBefore(\n    items,\n    fromIndex,\n    fromIndex + (offset < 0 ? offset : offset + 1),\n  );\n}\n\nexport function locateMenuItem(\n  sections: readonly MenuSection[],\n  flatIndex: number,\n): { sectionIndex: number; itemIndex: number; items: MenuItemData[] } | null {\n  let remaining = flatIndex;\n  for (let sectionIndex = 0; sectionIndex < sections.length; sectionIndex += 1) {\n    const section = sections[sectionIndex];\n    if (isMenuPickerSection(section)) continue;\n    if (remaining < section.items.length) {\n      return { sectionIndex, itemIndex: remaining, items: section.items };\n    }\n    remaining -= section.items.length;\n  }\n  return null;\n}\n\nexport function createMenuReorderDetail(\n  items: MenuItemData[],\n  fromIndex: number,\n  nextItems: MenuItemData[],\n  sectionIndex: number,\n): MenuReorderDetail | null {\n  const item = items[fromIndex];\n  if (!item) return null;\n  const toIndex = nextItems.indexOf(item);\n  if (toIndex < 0) {\n    const value = item.value;\n    const matched = value\n      ? nextItems.findIndex(candidate => candidate.value === value)\n      : -1;\n    if (matched < 0) return null;\n    return { item, fromIndex, toIndex: matched, sectionIndex, items: nextItems };\n  }\n  return { item, fromIndex, toIndex, sectionIndex, items: nextItems };\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "src/wc/components/Menu/menu-sections.ts",
      "content": "import type { MenuSection } from './menu-types';\nimport { isMenuSwatchPickerSection } from './menu-types';\n\n/** Preserve the exact menu context painted during exit motion. */\nexport function snapshotMenuSections(sections: readonly MenuSection[]): MenuSection[] {\n  return sections.map(section => {\n    if (isMenuSwatchPickerSection(section)) {\n      return {\n        ...section,\n        ...(section.options\n          ? { options: section.options.map(option => ({ ...option })) }\n          : {}),\n        ...(section.sections\n          ? {\n              sections: section.sections.map(swatchSection => ({\n                ...swatchSection,\n                options: swatchSection.options.map(option => ({ ...option })),\n              })),\n            }\n          : {}),\n      };\n    }\n    return { ...section, items: section.items.map(item => ({ ...item })) };\n  });\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "src/wc/components/Menu/menu-types.ts",
      "content": "import type { SwatchPickerOption, SwatchPickerSection } from '../SwatchPicker/swatch-picker-types';\nimport type { TagContrast, TagIntent } from '../Tag/Tag';\n\nexport type { MenuAlign, MenuSide } from './menu-position';\nexport {\n  PANEL_NAV_USER_MENU_PLACEMENT,\n  PANEL_TOOLS_HEADER_MENU_PLACEMENT,\n  type MenuPlacement,\n} from './menu-placement';\n\nexport interface MenuItemTagData {\n  label: string;\n  intent?: TagIntent;\n  contrast?: TagContrast;\n  rounded?: boolean;\n}\n\nexport interface MenuItemData {\n  label: string;\n  value?: string;\n  subtext?: string;\n  /** Leading decorative icon. Ignored when `reorderable` is true. */\n  icon?: string;\n  /** Show a supplemental brand notification dot. */\n  dot?: boolean;\n  /** Show a non-interactive tag at the trailing edge of the row. */\n  tag?: MenuItemTagData;\n  isSelected?: boolean;\n  isInactive?: boolean;\n  isDestructive?: boolean;\n  /** Render a compact switch indicator; the menu row owns interaction. */\n  showSwitch?: boolean;\n  switchValue?: boolean;\n  /** Prefix a drag handle and allow pointer and keyboard reorder within the contiguous reorderable run. */\n  reorderable?: boolean;\n}\n\n/** Next section item order after a pointer or keyboard reorder. */\nexport interface MenuReorderDetail {\n  item: MenuItemData;\n  fromIndex: number;\n  toIndex: number;\n  sectionIndex: number;\n  items: MenuItemData[];\n}\n\nexport interface MenuItemsSection {\n  header?: string;\n  items: MenuItemData[];\n}\n\nexport interface MenuSwatchPickerSection {\n  header?: string;\n  variant: 'swatch-picker';\n  value: string;\n  options?: SwatchPickerOption[];\n  sections?: SwatchPickerSection[];\n  groupLabel?: string;\n}\n\nexport type MenuSection =\n  | MenuItemsSection\n  | MenuSwatchPickerSection;\n\nexport function isMenuSwatchPickerSection(\n  section: MenuSection,\n): section is MenuSwatchPickerSection {\n  return 'variant' in section && section.variant === 'swatch-picker';\n}\n\nexport function isMenuPickerSection(\n  section: MenuSection,\n): section is MenuSwatchPickerSection {\n  return isMenuSwatchPickerSection(section);\n}\n\nexport type { SwatchPickerOption, SwatchPickerSection };\n",
      "type": "registry:ui"
    }
  ]
}
