{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "filter-menu",
  "title": "FilterMenu",
  "description": "Controlled two-pane filter select with menu-recipe category navigation, single- and multiple-choice, boolean, and date filters, selected counts, and a clear action.",
  "type": "registry:ui",
  "meta": {
    "library": "@ds-mo/ui",
    "distribution": "npm",
    "storybook": "https://zainadeel.github.io/compomo/?path=/story/filter-menu",
    "source": "src/wc/components/FilterMenu/FilterMenu.tsx",
    "intentStatus": "complete",
    "intent": {
      "audience": "general",
      "status": "stable",
      "summary": "Controlled two-pane filter select with menu-recipe category navigation, single- and multiple-choice, boolean, and date filters, selected counts, and a clear action.",
      "useWhen": [
        "A compact trigger must expose several filter families without turning the toolbar into many separate controls.",
        "Users need to move between persistent filter categories while editing the selected category in a second pane."
      ],
      "avoidWhen": [
        "The user is selecting one form value; use Select.",
        "The popup contains contextual commands rather than filtering controls; use Menu.",
        "The workflow requires confirmation or several ordered steps; use Modal."
      ],
      "alternatives": [
        {
          "when": "One known value is selected for a form field.",
          "component": "component:ds-select",
          "reason": "Select owns compact form-value and listbox semantics."
        },
        {
          "when": "A trigger reveals commands or row actions.",
          "component": "component:ds-menu",
          "reason": "Menu provides the appropriate action-menu semantics and keyboard model."
        }
      ],
      "commonlyComposedWith": [
        "component:ds-select",
        "component:ds-table",
        "component:ds-checkbox"
      ],
      "accessibility": [
        "The built-in select trigger exposes combobox semantics with a dialog popup, synchronizes expanded state, holds the pressed interaction wash without changing resting foreground while open, and restores focus after dismissal.",
        "The category pane is a vertical tab list. Arrow Up and Arrow Down wrap, Home and End jump, Arrow Right enters the option pane, and Arrow Left returns to the active category.",
        "Escape and outside activation close the popup and restore trigger focus. Tab moves through option controls and closes only when it leaves the popup boundary.",
        "The trigger count reports active filter categories. Category counts and the footer report individual selected criteria; none replace visible selected-state indication."
      ],
      "states": [
        "The application owns filters, values, activeFilterId, and open. Replace controlled objects and arrays after dsChange, dsActiveFilterChange, dsClear, or dsClose.",
        "Single filters return a string, multiple filters return ordered string arrays, boolean filters return a boolean, and date filters return a YYYY-MM-DD string.",
        "Selection applies immediately and does not close the popup.",
        "Changing category replaces the option-pane subtree so checkbox and other option visuals mount in their new state instead of transitioning from the previous category's row state.",
        "The trigger reports how many filter categories have selections, while the footer appears when values are selected and reports the total selected criteria."
      ],
      "responsiveBehavior": [
        "The popup defaults to the large menu-width token and clamps to the viewport.",
        "When collapseLabel is set inside a named ds-table container narrower than 900px, the trigger omits its label and chevron and becomes icon-only. Active criteria promote the prefix icon to primary.",
        "Both panes remain visible at narrow widths, with a smaller category minimum and reduced option-pane padding.",
        "Treat side as preferred placement; the popup flips and clamps when viewport collision requires it."
      ],
      "frameworkCaveats": {
        "customElements": [
          "Assign filters and values as JavaScript properties after the element is defined."
        ],
        "react": [
          "Pass filters and values as controlled props and handle onDsChange, onDsActiveFilterChange, onDsClear, and onDsClose."
        ],
        "angular": [
          "Use property bindings for filters and values and event bindings for dsChange, dsActiveFilterChange, dsClear, and dsClose."
        ]
      },
      "references": [
        {
          "label": "Storybook example",
          "path": "src/wc/components/FilterMenu/FilterMenu.stories.ts"
        }
      ]
    },
    "api": {
      "props": {
        "activeFill": {
          "type": "boolean",
          "resolvedType": "boolean",
          "attribute": "active-fill",
          "default": "false",
          "required": false,
          "mutable": false,
          "description": "Show selected interaction fill when one or more criteria are active."
        },
        "activeFilterId": {
          "type": "string | undefined",
          "resolvedType": "string | undefined",
          "attribute": "active-filter-id",
          "required": false,
          "mutable": false,
          "description": "Controlled category shown in the option pane."
        },
        "align": {
          "type": "AnchoredAlign",
          "resolvedType": "\"center\" | \"end\" | \"start\"",
          "attribute": "align",
          "default": "'end'",
          "required": false,
          "mutable": false,
          "description": "Cross-axis alignment against the trigger."
        },
        "alignOffset": {
          "type": "number | string",
          "resolvedType": "number | string",
          "attribute": "align-offset",
          "default": "0",
          "required": false,
          "mutable": false,
          "description": "Cross-axis offset — number in px or a TokoMo length."
        },
        "anchor": {
          "type": "HTMLElement | undefined",
          "resolvedType": "HTMLElement | undefined",
          "required": false,
          "mutable": false,
          "description": "External trigger element to position against."
        },
        "anchorId": {
          "type": "string | undefined",
          "resolvedType": "string | undefined",
          "attribute": "anchor-id",
          "required": false,
          "mutable": false,
          "description": "ID of the external trigger element used for positioning and focus return."
        },
        "ariaLabel": {
          "type": "string | null",
          "resolvedType": "null | string",
          "attribute": "aria-label",
          "default": "null",
          "required": false,
          "mutable": false,
          "description": "Direct accessible name for the internal select trigger."
        },
        "categoriesLabel": {
          "type": "string",
          "resolvedType": "string",
          "attribute": "categories-label",
          "default": "'Filter categories'",
          "required": false,
          "mutable": false,
          "description": "Accessible name for the category tab list."
        },
        "clearLabel": {
          "type": "string",
          "resolvedType": "string",
          "attribute": "clear-label",
          "default": "'Clear'",
          "required": false,
          "mutable": false,
          "description": "Footer action and date-clear accessible label."
        },
        "collapseLabel": {
          "type": "boolean",
          "resolvedType": "boolean",
          "attribute": "collapse-label",
          "default": "false",
          "required": false,
          "mutable": false,
          "description": "Opt into table-caption icon-only chrome below 900px. The trigger omits its\nvisible label and chevron; keep an accessible name via aria-label."
        },
        "filters": {
          "type": "FilterMenuFilter[]",
          "resolvedType": "FilterMenuFilter[]",
          "default": "[]",
          "required": false,
          "mutable": false,
          "description": "Product-owned filter categories and option definitions."
        },
        "hasBorder": {
          "type": "boolean",
          "resolvedType": "boolean",
          "attribute": "has-border",
          "default": "true",
          "required": false,
          "mutable": false,
          "description": "Show the surface-aware inset border around the select trigger."
        },
        "icon": {
          "type": "string | undefined",
          "resolvedType": "string | undefined",
          "attribute": "icon",
          "default": "'Filters'",
          "required": false,
          "mutable": false,
          "description": "Optional select trigger prefix icon name."
        },
        "initialFocusVisible": {
          "type": "boolean",
          "resolvedType": "boolean",
          "attribute": "initial-focus-visible",
          "default": "false",
          "required": false,
          "mutable": false,
          "description": "Show a visible focus ring on initial entry after keyboard activation."
        },
        "inputId": {
          "type": "string | undefined",
          "resolvedType": "string | undefined",
          "attribute": "input-id",
          "required": false,
          "mutable": false,
          "description": "ID applied to the internal select trigger."
        },
        "menuLabel": {
          "type": "string",
          "resolvedType": "string",
          "attribute": "menu-label",
          "default": "'Filters'",
          "required": false,
          "mutable": false,
          "description": "Accessible name for the non-modal filter dialog."
        },
        "menuWidth": {
          "type": "string",
          "resolvedType": "string",
          "attribute": "menu-width",
          "default": "TOKEN_CSS_LENGTHS.menuWidthLg",
          "required": false,
          "mutable": false,
          "description": "Popup width. It remains clamped to the viewport by the component recipe."
        },
        "open": {
          "type": "boolean",
          "resolvedType": "boolean",
          "attribute": "open",
          "default": "false",
          "required": false,
          "mutable": true,
          "description": "Controlled popup visibility."
        },
        "side": {
          "type": "AnchoredSide",
          "resolvedType": "\"bottom\" | \"left\" | \"right\" | \"top\"",
          "attribute": "side",
          "default": "'bottom'",
          "required": false,
          "mutable": false,
          "description": "Preferred popup side; collision handling may flip it."
        },
        "sideOffset": {
          "type": "number | string",
          "resolvedType": "number | string",
          "attribute": "side-offset",
          "default": "TOKEN_CSS_LENGTHS.space050",
          "required": false,
          "mutable": false,
          "description": "Gap between trigger and popup — number in px or a TokoMo length."
        },
        "size": {
          "type": "FilterMenuSize",
          "resolvedType": "\"lg\" | \"md\" | \"sm\" | \"xs\"",
          "attribute": "size",
          "default": "'md'",
          "required": false,
          "mutable": false,
          "description": "Select trigger density."
        },
        "triggerLabel": {
          "type": "string",
          "resolvedType": "string",
          "attribute": "trigger-label",
          "default": "'Filters'",
          "required": false,
          "mutable": false,
          "description": "Select trigger text. The selected count is appended automatically."
        },
        "values": {
          "type": "FilterMenuValues",
          "resolvedType": "{ [x: string]: FilterMenuValue | undefined; }",
          "default": "{}",
          "required": false,
          "mutable": false,
          "description": "Controlled values keyed by filter id."
        },
        "width": {
          "type": "FilterMenuWidth",
          "resolvedType": "\"fill\" | \"hug\"",
          "attribute": "width",
          "default": "'hug'",
          "required": false,
          "mutable": false,
          "description": "Select trigger width fit."
        }
      },
      "events": [
        {
          "name": "dsActiveFilterChange",
          "detail": "string",
          "bubbles": true,
          "cancelable": true,
          "composed": true,
          "description": "Requests a controlled active-category replacement."
        },
        {
          "name": "dsAfterClose",
          "detail": "void",
          "bubbles": true,
          "cancelable": true,
          "composed": true,
          "description": "Emitted after exit motion and rendered popup removal complete."
        },
        {
          "name": "dsChange",
          "detail": "FilterMenuChangeDetail",
          "bubbles": true,
          "cancelable": true,
          "composed": true,
          "description": "Requests a controlled value replacement without closing the popup."
        },
        {
          "name": "dsClear",
          "detail": "void",
          "bubbles": true,
          "cancelable": true,
          "composed": true,
          "description": "Requests that the consumer clear every filter value."
        },
        {
          "name": "dsClose",
          "detail": "void",
          "bubbles": true,
          "cancelable": true,
          "composed": true,
          "description": "Requests that the controlled popup close."
        },
        {
          "name": "dsOpenChange",
          "detail": "boolean",
          "bubbles": true,
          "cancelable": true,
          "composed": true,
          "description": "Emitted whenever internal select-trigger activation changes popup visibility."
        }
      ],
      "methods": [],
      "slots": []
    },
    "props": {
      "activeFill": {
        "type": "boolean",
        "resolvedType": "boolean",
        "attribute": "active-fill",
        "default": "false",
        "required": false,
        "mutable": false,
        "description": "Show selected interaction fill when one or more criteria are active."
      },
      "activeFilterId": {
        "type": "string | undefined",
        "resolvedType": "string | undefined",
        "attribute": "active-filter-id",
        "required": false,
        "mutable": false,
        "description": "Controlled category shown in the option pane."
      },
      "align": {
        "type": "AnchoredAlign",
        "resolvedType": "\"center\" | \"end\" | \"start\"",
        "attribute": "align",
        "default": "'end'",
        "required": false,
        "mutable": false,
        "description": "Cross-axis alignment against the trigger."
      },
      "alignOffset": {
        "type": "number | string",
        "resolvedType": "number | string",
        "attribute": "align-offset",
        "default": "0",
        "required": false,
        "mutable": false,
        "description": "Cross-axis offset — number in px or a TokoMo length."
      },
      "anchor": {
        "type": "HTMLElement | undefined",
        "resolvedType": "HTMLElement | undefined",
        "required": false,
        "mutable": false,
        "description": "External trigger element to position against."
      },
      "anchorId": {
        "type": "string | undefined",
        "resolvedType": "string | undefined",
        "attribute": "anchor-id",
        "required": false,
        "mutable": false,
        "description": "ID of the external trigger element used for positioning and focus return."
      },
      "ariaLabel": {
        "type": "string | null",
        "resolvedType": "null | string",
        "attribute": "aria-label",
        "default": "null",
        "required": false,
        "mutable": false,
        "description": "Direct accessible name for the internal select trigger."
      },
      "categoriesLabel": {
        "type": "string",
        "resolvedType": "string",
        "attribute": "categories-label",
        "default": "'Filter categories'",
        "required": false,
        "mutable": false,
        "description": "Accessible name for the category tab list."
      },
      "clearLabel": {
        "type": "string",
        "resolvedType": "string",
        "attribute": "clear-label",
        "default": "'Clear'",
        "required": false,
        "mutable": false,
        "description": "Footer action and date-clear accessible label."
      },
      "collapseLabel": {
        "type": "boolean",
        "resolvedType": "boolean",
        "attribute": "collapse-label",
        "default": "false",
        "required": false,
        "mutable": false,
        "description": "Opt into table-caption icon-only chrome below 900px. The trigger omits its\nvisible label and chevron; keep an accessible name via aria-label."
      },
      "filters": {
        "type": "FilterMenuFilter[]",
        "resolvedType": "FilterMenuFilter[]",
        "default": "[]",
        "required": false,
        "mutable": false,
        "description": "Product-owned filter categories and option definitions."
      },
      "hasBorder": {
        "type": "boolean",
        "resolvedType": "boolean",
        "attribute": "has-border",
        "default": "true",
        "required": false,
        "mutable": false,
        "description": "Show the surface-aware inset border around the select trigger."
      },
      "icon": {
        "type": "string | undefined",
        "resolvedType": "string | undefined",
        "attribute": "icon",
        "default": "'Filters'",
        "required": false,
        "mutable": false,
        "description": "Optional select trigger prefix icon name."
      },
      "initialFocusVisible": {
        "type": "boolean",
        "resolvedType": "boolean",
        "attribute": "initial-focus-visible",
        "default": "false",
        "required": false,
        "mutable": false,
        "description": "Show a visible focus ring on initial entry after keyboard activation."
      },
      "inputId": {
        "type": "string | undefined",
        "resolvedType": "string | undefined",
        "attribute": "input-id",
        "required": false,
        "mutable": false,
        "description": "ID applied to the internal select trigger."
      },
      "menuLabel": {
        "type": "string",
        "resolvedType": "string",
        "attribute": "menu-label",
        "default": "'Filters'",
        "required": false,
        "mutable": false,
        "description": "Accessible name for the non-modal filter dialog."
      },
      "menuWidth": {
        "type": "string",
        "resolvedType": "string",
        "attribute": "menu-width",
        "default": "TOKEN_CSS_LENGTHS.menuWidthLg",
        "required": false,
        "mutable": false,
        "description": "Popup width. It remains clamped to the viewport by the component recipe."
      },
      "open": {
        "type": "boolean",
        "resolvedType": "boolean",
        "attribute": "open",
        "default": "false",
        "required": false,
        "mutable": true,
        "description": "Controlled popup visibility."
      },
      "side": {
        "type": "AnchoredSide",
        "resolvedType": "\"bottom\" | \"left\" | \"right\" | \"top\"",
        "attribute": "side",
        "default": "'bottom'",
        "required": false,
        "mutable": false,
        "description": "Preferred popup side; collision handling may flip it."
      },
      "sideOffset": {
        "type": "number | string",
        "resolvedType": "number | string",
        "attribute": "side-offset",
        "default": "TOKEN_CSS_LENGTHS.space050",
        "required": false,
        "mutable": false,
        "description": "Gap between trigger and popup — number in px or a TokoMo length."
      },
      "size": {
        "type": "FilterMenuSize",
        "resolvedType": "\"lg\" | \"md\" | \"sm\" | \"xs\"",
        "attribute": "size",
        "default": "'md'",
        "required": false,
        "mutable": false,
        "description": "Select trigger density."
      },
      "triggerLabel": {
        "type": "string",
        "resolvedType": "string",
        "attribute": "trigger-label",
        "default": "'Filters'",
        "required": false,
        "mutable": false,
        "description": "Select trigger text. The selected count is appended automatically."
      },
      "values": {
        "type": "FilterMenuValues",
        "resolvedType": "{ [x: string]: FilterMenuValue | undefined; }",
        "default": "{}",
        "required": false,
        "mutable": false,
        "description": "Controlled values keyed by filter id."
      },
      "width": {
        "type": "FilterMenuWidth",
        "resolvedType": "\"fill\" | \"hug\"",
        "attribute": "width",
        "default": "'hug'",
        "required": false,
        "mutable": false,
        "description": "Select trigger width fit."
      }
    },
    "events": [
      {
        "name": "dsActiveFilterChange",
        "detail": "string",
        "bubbles": true,
        "cancelable": true,
        "composed": true,
        "description": "Requests a controlled active-category replacement."
      },
      {
        "name": "dsAfterClose",
        "detail": "void",
        "bubbles": true,
        "cancelable": true,
        "composed": true,
        "description": "Emitted after exit motion and rendered popup removal complete."
      },
      {
        "name": "dsChange",
        "detail": "FilterMenuChangeDetail",
        "bubbles": true,
        "cancelable": true,
        "composed": true,
        "description": "Requests a controlled value replacement without closing the popup."
      },
      {
        "name": "dsClear",
        "detail": "void",
        "bubbles": true,
        "cancelable": true,
        "composed": true,
        "description": "Requests that the consumer clear every filter value."
      },
      {
        "name": "dsClose",
        "detail": "void",
        "bubbles": true,
        "cancelable": true,
        "composed": true,
        "description": "Requests that the controlled popup close."
      },
      {
        "name": "dsOpenChange",
        "detail": "boolean",
        "bubbles": true,
        "cancelable": true,
        "composed": true,
        "description": "Emitted whenever internal select-trigger activation changes popup visibility."
      }
    ],
    "methods": [],
    "slots": [],
    "exports": {
      "customElement": "ds-filter-menu",
      "react": "DsFilterMenu",
      "vue": "DsFilterMenu",
      "angular": "DsFilterMenu"
    },
    "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-filter-menu.js';",
        "example": "<ds-filter-menu></ds-filter-menu>"
      },
      "react": {
        "import": "import { DsFilterMenu } from '@ds-mo/ui/react';",
        "example": "<DsFilterMenu />"
      },
      "vue": {
        "import": "import { DsFilterMenu } from '@ds-mo/ui/vue';",
        "example": "<DsFilterMenu />"
      },
      "angular": {
        "import": "import { DsFilterMenu } from '@ds-mo/ui/angular/ds-filter-menu';",
        "example": "<ds-filter-menu></ds-filter-menu>"
      },
      "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: anchor, filters, values.",
      "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": [
    "button-unfilled",
    "checkbox",
    "icon",
    "tag",
    "text",
    "tooltip"
  ],
  "files": [
    {
      "path": "src/wc/components/FilterMenu/FilterMenu.css",
      "content": "@import '../../utils/choice-list.css';\n@import '../../utils/control-parts.css';\n@import '../../utils/control-width.css';\n@import '../../utils/select-trigger.css';\n@import '../../utils/table-caption-control.css';\n\n:host(.filter-menu-host--external-trigger) {\n  display: contents;\n}\n\n.filter-menu {\n  display: flex;\n  min-height: 0;\n  flex-direction: column;\n  color: var(--color-foreground-primary);\n}\n\n.filter-menu__body {\n  display: grid;\n  grid-template-columns: minmax(calc(var(--dimension-size-base) * 18), 2fr) minmax(0, 3fr);\n  min-height: calc(var(--dimension-size-800) * 4 + var(--dimension-space-150));\n  max-height: calc(100vh - var(--dimension-size-800) - var(--dimension-size-600));\n}\n\n.filter-menu__categories {\n  min-width: 0;\n  border-inline-end: var(--dimension-stroke-width-012) solid var(--color-border-tertiary);\n}\n\n.filter-menu__category-section {\n  display: flex;\n  min-width: 0;\n  flex-direction: column;\n}\n\n.filter-menu__category-section.ds-choice-section--divided {\n  padding-block-end: var(--dimension-space-050);\n}\n\n.filter-menu__category {\n  justify-content: space-between;\n  width: 100%;\n  border: 0;\n  background: transparent;\n  color: var(--color-foreground-secondary);\n  cursor: default;\n  text-align: start;\n}\n\n.filter-menu__category--active {\n  color: var(--color-foreground-primary);\n}\n\n.filter-menu__options {\n  min-width: 0;\n  overflow: hidden;\n}\n\n.filter-menu__date-field {\n  display: flex;\n  min-width: 0;\n  flex-direction: column;\n  align-items: stretch;\n  gap: var(--dimension-space-100);\n}\n\n.filter-menu__date-control {\n  width: 100%;\n  border-radius: var(--dimension-radius-025);\n  background: transparent;\n  box-shadow: inset 0 0 0 var(--dimension-stroke-width-012) var(--color-border-secondary);\n}\n\n.filter-menu__date-control:focus-within {\n  box-shadow: inset 0 0 0 var(--dimension-stroke-width-018) var(--color-border-bold-brand);\n}\n\n.filter-menu__date-input {\n  flex: 1 1 auto;\n  min-width: 0;\n  height: 100%;\n  padding-inline: var(--dimension-space-100);\n  border: 0;\n  outline: 0;\n  background: transparent;\n  color: var(--color-foreground-primary);\n  font: inherit;\n}\n\n.filter-menu__footer {\n  flex: 0 0 auto;\n}\n\n.filter-menu__clear {\n  color: var(--color-foreground-bold-brand);\n}\n\n@media (max-width: 520px) {\n  .filter-menu__body {\n    grid-template-columns: minmax(calc(var(--dimension-size-base) * 14), 2fr) minmax(0, 3fr);\n  }\n}\n\n@media (forced-colors: active) {\n  .filter-menu__categories {\n    border-inline-end-color: var(--ds-forced-color-content);\n  }\n\n  .filter-menu__date-control {\n    outline: var(--dimension-stroke-width-012) solid var(--ds-forced-color-content);\n  }\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "src/wc/components/FilterMenu/FilterMenu.tsx",
      "content": "import {\n  Component,\n  Element,\n  Event,\n  EventEmitter,\n  h,\n  Host,\n  Listen,\n  Prop,\n  State,\n  Watch,\n} from '@stencil/core';\nimport {\n  CONTROL_TEXT_VARIANT,\n  controlWidthClass,\n  resolveChoicePopupAlignOffset,\n  resolveCssLengthPx,\n  resolveMotionTimeMs,\n  TOKEN_CSS_LENGTHS,\n  TOKEN_DEFAULTS,\n  type ControlWidth,\n} from '../../utils';\nimport { observeTableCaptionCompact } from '../../utils/table-caption-compact';\nimport type { AnchoredAlign, AnchoredSide } from '../../utils/anchored-position';\nimport { AnchoredPositionController } from '../../utils/anchored-position-controller';\nimport { ChoiceOptionRow } from '../../utils/choice-list-parts';\nimport { choiceListUsesSubtext } from '../../utils/choice-list';\n\nexport type FilterMenuFilterKind = 'single' | 'multiple' | 'boolean' | 'date';\nexport type FilterMenuSize = 'lg' | 'md' | 'sm' | 'xs';\nexport type FilterMenuWidth = ControlWidth;\n\nexport interface FilterMenuOption {\n  label: string;\n  value: string;\n  description?: string;\n  isInactive?: boolean;\n}\n\nexport interface FilterMenuFilter {\n  id: string;\n  label: string;\n  kind: FilterMenuFilterKind;\n  options?: FilterMenuOption[];\n  description?: string;\n  fieldLabel?: string;\n  /** Draw a standard menu-section divider after this category. */\n  divider?: boolean;\n}\n\nexport type FilterMenuValue = string[] | boolean | string;\nexport type FilterMenuValues = Record<string, FilterMenuValue | undefined>;\n\nexport interface FilterMenuChangeDetail {\n  filterId: string;\n  value: FilterMenuValue;\n}\n\nconst FILTER_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\nlet filterMenuId = 0;\n\nconst ICON_SIZE: Record<FilterMenuSize, 'lg' | 'md' | 'sm' | 'xs'> = {\n  lg: 'lg',\n  md: 'md',\n  sm: 'sm',\n  xs: 'xs',\n};\n\n@Component({\n  tag: 'ds-filter-menu',\n  styleUrl: 'FilterMenu.css',\n  scoped: true,\n})\nexport class FilterMenu {\n  @Element() el!: HTMLElement;\n\n  /** Controlled popup visibility. */\n  @Prop({ mutable: true }) open: boolean = false;\n  /** Select trigger text. The selected count is appended automatically. */\n  @Prop() triggerLabel: string = 'Filters';\n  /** Optional select trigger prefix icon name. */\n  @Prop() icon: string | undefined = 'Filters';\n  /** Select trigger density. */\n  @Prop() size: FilterMenuSize = 'md';\n  /** Select trigger width fit. */\n  @Prop() width: FilterMenuWidth = 'hug';\n  /** Show the surface-aware inset border around the select trigger. */\n  @Prop() hasBorder: boolean = true;\n  /** Show selected interaction fill when one or more criteria are active. */\n  @Prop() activeFill: boolean = false;\n  /**\n   * Opt into table-caption icon-only chrome below 900px. The trigger omits its\n   * visible label and chevron; keep an accessible name via aria-label.\n   */\n  @Prop({ reflect: true }) collapseLabel: boolean = false;\n  /** ID applied to the internal select trigger. */\n  @Prop() inputId: string | undefined;\n  /** Direct accessible name for the internal select trigger. */\n  @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n  /** Product-owned filter categories and option definitions. */\n  @Prop() filters: FilterMenuFilter[] = [];\n  /** Controlled values keyed by filter id. */\n  @Prop() values: FilterMenuValues = {};\n  /** Controlled category shown in the option pane. */\n  @Prop() activeFilterId: string | undefined;\n  /** External trigger element to position against. */\n  @Prop() anchor: HTMLElement | undefined;\n  /** ID of the external trigger element used for positioning and focus return. */\n  @Prop() anchorId: string | undefined;\n  /** Preferred popup side; collision handling may flip it. */\n  @Prop() side: AnchoredSide = 'bottom';\n  /** Cross-axis alignment against the trigger. */\n  @Prop() align: AnchoredAlign = 'end';\n  /** Gap between trigger and popup — number in px or a TokoMo length. */\n  @Prop() sideOffset: number | string = TOKEN_CSS_LENGTHS.space050;\n  /** Cross-axis offset — number in px or a TokoMo length. */\n  @Prop() alignOffset: number | string = 0;\n  /** Popup width. It remains clamped to the viewport by the component recipe. */\n  @Prop() menuWidth: string = TOKEN_CSS_LENGTHS.menuWidthLg;\n  /** Accessible name for the non-modal filter dialog. */\n  @Prop() menuLabel: string = 'Filters';\n  /** Accessible name for the category tab list. */\n  @Prop() categoriesLabel: string = 'Filter categories';\n  /** Footer action and date-clear accessible label. */\n  @Prop() clearLabel: string = 'Clear';\n  /** Show a visible focus ring on initial entry after keyboard activation. */\n  @Prop() initialFocusVisible: boolean = false;\n\n  @State() private shouldRender = false;\n  @State() private closing = false;\n  @State() private positionReady = false;\n  @State() private pos: { x: number; y: number } = { x: 0, y: 0 };\n  @State() private focusRingVisible = false;\n  @State() private activeOptionIndex = 0;\n  @State() private captionCompact = false;\n\n  /** Requests a controlled value replacement without closing the popup. */\n  @Event() dsChange!: EventEmitter<FilterMenuChangeDetail>;\n  /** Requests that the consumer clear every filter value. */\n  @Event() dsClear!: EventEmitter<void>;\n  /** Requests a controlled active-category replacement. */\n  @Event() dsActiveFilterChange!: EventEmitter<string>;\n  /** Requests that the controlled popup close. */\n  @Event() dsClose!: EventEmitter<void>;\n  /** Emitted whenever internal select-trigger activation changes popup visibility. */\n  @Event() dsOpenChange!: EventEmitter<boolean>;\n  /** Emitted after exit motion and rendered popup removal complete. */\n  @Event() dsAfterClose!: EventEmitter<void>;\n\n  private readonly generatedId = `ds-filter-menu-${++filterMenuId}`;\n  private outsideHandler: ((event: MouseEvent) => void) | null = null;\n  private triggerElement: HTMLButtonElement | null = null;\n  private pendingInitialFocusVisible = false;\n  private closeTimer: ReturnType<typeof setTimeout> | null = null;\n  private captionCompactDisconnect: (() => void) | undefined;\n  private hasLoaded = false;\n  private closingSnapshot: {\n    filters: FilterMenuFilter[];\n    values: FilterMenuValues;\n    activeFilterId: string | undefined;\n  } | null = null;\n\n  private readonly position = new AnchoredPositionController({\n    getAnchor: () => this.resolvedAnchor,\n    getPopup: () => this.el.querySelector<HTMLElement>('.filter-menu-popup'),\n    measure: (anchor, popup) => {\n      if (!this.open) return null;\n      const anchorRect = anchor.getBoundingClientRect();\n      const sectionInsetPx = resolveCssLengthPx(TOKEN_DEFAULTS.space050, TOKEN_DEFAULTS.space050);\n      return {\n        anchorRect,\n        popupWidth: popup.offsetWidth || this.popupFallbackWidth,\n        popupHeight: popup.offsetHeight || this.popupFallbackHeight,\n        side: this.side,\n        align: this.align,\n        sideOffsetPx: resolveCssLengthPx(this.sideOffset, TOKEN_DEFAULTS.space050),\n        alignOffsetPx: resolveChoicePopupAlignOffset({\n          align: this.align,\n          alignOffsetPx: resolveCssLengthPx(this.alignOffset, 0),\n          sectionInsetPx,\n        }),\n        viewportPadPx: this.viewportPad,\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    observeResize: true,\n    topLayer: true,\n  });\n\n  componentDidLoad() {\n    this.hasLoaded = true;\n    this.syncCaptionCompactObserver();\n    if (this.open) this.onOpenChange(true);\n  }\n\n  connectedCallback() {\n    if (!this.hasLoaded) return;\n    this.syncCaptionCompactObserver();\n    if (this.open) this.onOpenChange(true);\n  }\n\n  disconnectedCallback() {\n    this.disconnectCaptionCompactObserver();\n    this.position.unobserve();\n    this.teardownListeners();\n  }\n\n  @Watch('collapseLabel')\n  onCollapseLabelChange() {\n    this.syncCaptionCompactObserver();\n  }\n\n  @Watch('open')\n  onOpenChange(isOpen: boolean) {\n    if (isOpen) {\n      this.teardownListeners();\n      this.closingSnapshot = null;\n      this.shouldRender = true;\n      this.closing = false;\n      this.positionReady = false;\n      this.focusRingVisible = this.pendingInitialFocusVisible || this.initialFocusVisible;\n      this.setupListeners();\n      this.position.schedule(() => this.focusInitialCategory());\n      return;\n    }\n\n    if (!this.shouldRender) return;\n    this.position.cancel();\n    this.captureClosingSnapshot();\n    this.closing = true;\n    this.teardownListeners();\n    const duration = resolveMotionTimeMs(\n      TOKEN_DEFAULTS.motionShort2,\n      TOKEN_DEFAULTS.animationDurationShort3\n    );\n    if (duration <= 0) {\n      this.finishClose();\n      return;\n    }\n    this.closeTimer = setTimeout(() => this.finishClose(), duration);\n  }\n\n  @Watch('anchor')\n  @Watch('anchorId')\n  @Watch('side')\n  @Watch('align')\n  @Watch('sideOffset')\n  @Watch('alignOffset')\n  @Watch('menuWidth')\n  onPositionInputChange() {\n    if (this.open) this.position.schedule();\n  }\n\n  @Listen('keydown')\n  handleKeyDown(event: KeyboardEvent) {\n    if (!this.shouldRender || this.closing) return;\n    if (event.key === 'Escape') {\n      event.preventDefault();\n      this.close();\n      return;\n    }\n    if (event.key !== 'Tab' || !this.tabLeavesPopup(event)) return;\n    event.preventDefault();\n    this.close(false);\n    this.moveFocusAfterTab(event.shiftKey);\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 this.triggerElement;\n  }\n\n  private get usesInternalTrigger(): boolean {\n    return !this.anchor && !this.anchorId;\n  }\n\n  private get anchorFocusTarget(): HTMLElement | null {\n    const anchor = this.resolvedAnchor;\n    if (!anchor) return null;\n    if (anchor.matches(FILTER_MENU_FOCUSABLE_SELECTOR)) return anchor;\n    return anchor.querySelector<HTMLElement>(FILTER_MENU_FOCUSABLE_SELECTOR) ?? anchor;\n  }\n\n  private get viewportPad(): number {\n    return resolveCssLengthPx(TOKEN_DEFAULTS.space050, TOKEN_DEFAULTS.space050);\n  }\n\n  private get popupFallbackWidth(): number {\n    return resolveCssLengthPx(TOKEN_DEFAULTS.menuWidthLg, TOKEN_DEFAULTS.menuWidthLg);\n  }\n\n  private get popupFallbackHeight(): number {\n    return resolveCssLengthPx(TOKEN_DEFAULTS.menuFallbackHeight, TOKEN_DEFAULTS.menuFallbackHeight);\n  }\n\n  private setupListeners() {\n    this.outsideHandler = event => {\n      const target = event.target as Node;\n      const popup = this.el.querySelector('.filter-menu-popup');\n      if (popup?.contains(target) || this.resolvedAnchor?.contains(target)) return;\n      this.close();\n    };\n    document.addEventListener('mousedown', this.outsideHandler, true);\n    this.position.observe();\n  }\n\n  private teardownListeners() {\n    this.position.unobserve();\n    if (this.outsideHandler) {\n      document.removeEventListener('mousedown', this.outsideHandler, true);\n      this.outsideHandler = null;\n    }\n    if (this.closeTimer) {\n      clearTimeout(this.closeTimer);\n      this.closeTimer = null;\n    }\n  }\n\n  private close(restoreFocus = true) {\n    this.captureClosingSnapshot();\n    if (restoreFocus) this.focusAnchor();\n    this.dsOpenChange.emit(false);\n    this.dsClose.emit();\n    this.open = false;\n    this.onOpenChange(false);\n  }\n\n  private toggleFromTrigger(event: MouseEvent) {\n    if (this.open) {\n      this.close();\n      return;\n    }\n    this.pendingInitialFocusVisible = event.detail === 0;\n    this.open = true;\n    this.dsOpenChange.emit(true);\n    this.onOpenChange(true);\n  }\n\n  private handleTriggerKeyDown(event: KeyboardEvent) {\n    if (event.key !== 'ArrowDown' || this.open) return;\n    event.preventDefault();\n    this.pendingInitialFocusVisible = true;\n    this.open = true;\n    this.dsOpenChange.emit(true);\n    this.onOpenChange(true);\n  }\n\n  private finishClose() {\n    const popup = this.el.querySelector<HTMLElement>('.filter-menu-popup');\n    if (popup?.matches(':popover-open')) popup.hidePopover();\n    this.shouldRender = false;\n    this.closing = false;\n    this.closingSnapshot = null;\n    this.closeTimer = null;\n    this.dsAfterClose.emit();\n  }\n\n  private captureClosingSnapshot() {\n    if (this.closingSnapshot) return;\n    this.closingSnapshot = {\n      filters: this.filters.map(filter => ({\n        ...filter,\n        options: filter.options?.map(option => ({ ...option })),\n      })),\n      values: Object.fromEntries(\n        Object.entries(this.values).map(([key, value]) => [\n          key,\n          Array.isArray(value) ? [...value] : value,\n        ])\n      ),\n      activeFilterId: this.activeFilterId,\n    };\n  }\n\n  private focusAnchor() {\n    const anchor = this.resolvedAnchor as\n      (HTMLElement & { setFocus?: () => Promise<void> | void }) | null;\n    if (anchor?.setFocus) {\n      void anchor.setFocus();\n      return;\n    }\n    this.anchorFocusTarget?.focus();\n  }\n\n  private focusInitialCategory() {\n    requestAnimationFrame(() => {\n      this.el.querySelector<HTMLElement>('.filter-menu__category[tabindex=\"0\"]')?.focus();\n      this.pendingInitialFocusVisible = false;\n    });\n  }\n\n  private tabLeavesPopup(event: KeyboardEvent): boolean {\n    const popup = this.el.querySelector<HTMLElement>('.filter-menu-popup');\n    if (!popup) return true;\n    const focusables = Array.from(\n      popup.querySelectorAll<HTMLElement>(FILTER_MENU_FOCUSABLE_SELECTOR)\n    ).filter(element => element.getClientRects().length > 0);\n    const index = focusables.indexOf(event.target as HTMLElement);\n    if (index < 0) return true;\n    return event.shiftKey ? index === 0 : index === focusables.length - 1;\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    const candidates = Array.from(\n      document.querySelectorAll<HTMLElement>(FILTER_MENU_FOCUSABLE_SELECTOR)\n    ).filter(\n      element =>\n        !element.closest('.filter-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 selectedFilter(filters: FilterMenuFilter[], activeFilterId: string | undefined) {\n    return filters.find(filter => filter.id === activeFilterId) ?? filters[0];\n  }\n\n  private selectedCount(filter: FilterMenuFilter, values: FilterMenuValues): number {\n    const value = values[filter.id];\n    if (filter.kind === 'multiple') return Array.isArray(value) ? value.length : 0;\n    if (filter.kind === 'boolean') return value === true ? 1 : 0;\n    return typeof value === 'string' && value.length > 0 ? 1 : 0;\n  }\n\n  private totalSelected(filters: FilterMenuFilter[], values: FilterMenuValues): number {\n    return filters.reduce((total, filter) => total + this.selectedCount(filter, values), 0);\n  }\n\n  private activeFilterCount(filters: FilterMenuFilter[], values: FilterMenuValues): number {\n    return filters.filter(filter => this.selectedCount(filter, values) > 0).length;\n  }\n\n  private selectCategory(filterId: string, focus = false) {\n    this.activeOptionIndex = 0;\n    this.dsActiveFilterChange.emit(filterId);\n    if (!focus) return;\n    requestAnimationFrame(() => {\n      this.el.querySelector<HTMLElement>(`[data-filter-category=\"${filterId}\"]`)?.focus();\n    });\n  }\n\n  private handleCategoryKeyDown(\n    event: KeyboardEvent,\n    filters: FilterMenuFilter[],\n    filterId: string\n  ) {\n    const currentIndex = filters.findIndex(filter => filter.id === filterId);\n    let nextIndex: number;\n    if (event.key === 'ArrowDown') nextIndex = (currentIndex + 1) % filters.length;\n    else if (event.key === 'ArrowUp')\n      nextIndex = (currentIndex - 1 + filters.length) % filters.length;\n    else if (event.key === 'Home') nextIndex = 0;\n    else if (event.key === 'End') nextIndex = filters.length - 1;\n    else if (event.key === 'ArrowRight') {\n      event.preventDefault();\n      requestAnimationFrame(() => {\n        this.el\n          .querySelector<HTMLElement>(\n            '.filter-menu__options [role=\"option\"], .filter-menu__options input'\n          )\n          ?.focus();\n      });\n      return;\n    } else return;\n\n    event.preventDefault();\n    this.focusRingVisible = true;\n    this.selectCategory(filters[nextIndex].id, true);\n  }\n\n  private handlePanelKeyDown(event: KeyboardEvent, filterId: string) {\n    if (event.key !== 'ArrowLeft') return;\n    event.preventDefault();\n    this.el.querySelector<HTMLElement>(`[data-filter-category=\"${filterId}\"]`)?.focus();\n  }\n\n  private handleOptionKeyDown(\n    event: KeyboardEvent,\n    filter: FilterMenuFilter,\n    option: FilterMenuOption,\n    index: number,\n    values: FilterMenuValues\n  ) {\n    const options = filter.options ?? [];\n    if (event.key === 'ArrowLeft') {\n      this.handlePanelKeyDown(event, filter.id);\n      return;\n    }\n    if (event.key === 'Enter' || event.key === ' ') {\n      event.preventDefault();\n      this.selectOption(filter, option, values);\n      return;\n    }\n    if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) return;\n    event.preventDefault();\n    const enabled = options\n      .map((candidate, optionIndex) => ({ candidate, optionIndex }))\n      .filter(({ candidate }) => !candidate.isInactive)\n      .map(({ optionIndex }) => optionIndex);\n    if (!enabled.length) return;\n    const current = enabled.indexOf(index);\n    let next: number;\n    if (event.key === 'Home') next = enabled[0];\n    else if (event.key === 'End') next = enabled[enabled.length - 1];\n    else {\n      const direction = event.key === 'ArrowDown' ? 1 : -1;\n      const start = current >= 0 ? current : direction === 1 ? -1 : 0;\n      next = enabled[(start + direction + enabled.length) % enabled.length];\n    }\n    this.activeOptionIndex = next;\n    this.focusRingVisible = true;\n    requestAnimationFrame(() => {\n      this.el\n        .querySelector<HTMLElement>(`#${this.generatedId}-${filter.id}-option-${next}`)\n        ?.focus();\n    });\n  }\n\n  private toggleMultiple(\n    filter: FilterMenuFilter,\n    option: FilterMenuOption,\n    checked: boolean,\n    values: FilterMenuValues\n  ) {\n    if (option.isInactive) return;\n    const current = Array.isArray(values[filter.id]) ? (values[filter.id] as string[]) : [];\n    const selected = new Set(current);\n    if (checked) selected.add(option.value);\n    else selected.delete(option.value);\n    const ordered = (filter.options ?? [])\n      .map(candidate => candidate.value)\n      .filter(value => selected.has(value));\n    this.dsChange.emit({ filterId: filter.id, value: ordered });\n  }\n\n  private selectOption(\n    filter: FilterMenuFilter,\n    option: FilterMenuOption,\n    values: FilterMenuValues\n  ) {\n    if (option.isInactive) return;\n    if (filter.kind === 'single') {\n      this.dsChange.emit({ filterId: filter.id, value: option.value });\n      return;\n    }\n    const selected = Array.isArray(values[filter.id])\n      ? (values[filter.id] as string[]).includes(option.value)\n      : false;\n    this.toggleMultiple(filter, option, !selected, values);\n  }\n\n  private renderOptions(filter: FilterMenuFilter, values: FilterMenuValues) {\n    const value = values[filter.id];\n    if (filter.kind === 'multiple' || filter.kind === 'single') {\n      const selected = Array.isArray(value) ? value : [];\n      const options = filter.options ?? [];\n      const usesSubtext = choiceListUsesSubtext(\n        options.map(option => ({\n          label: option.label,\n          value: option.value,\n          subtext: option.description,\n        }))\n      );\n      return options.map((option, index) => {\n        const isSelected =\n          filter.kind === 'multiple' ? selected.includes(option.value) : value === option.value;\n        return (\n          <ChoiceOptionRow\n            size=\"md\"\n            id={`${this.generatedId}-${filter.id}-option-${index}`}\n            option={{\n              label: option.label,\n              value: option.value,\n              subtext: option.description,\n              isInactive: option.isInactive,\n            }}\n            selected={isSelected}\n            active={index === this.activeOptionIndex}\n            focusRingVisible={this.focusRingVisible}\n            usesSubtext={usesSubtext}\n            tabIndex={index === this.activeOptionIndex ? 0 : -1}\n            leading={\n              filter.kind === 'multiple' ? (\n                <span\n                  class=\"ds-choice-item__icon ds-control-icon-box ds-interaction-fill__content\"\n                  aria-hidden=\"true\"\n                >\n                  <ds-checkbox label=\"\" size=\"md\" checked={isSelected} presentation />\n                </span>\n              ) : undefined\n            }\n            onFocus={() => {\n              this.activeOptionIndex = index;\n            }}\n            onKeyDown={(event: KeyboardEvent) =>\n              this.handleOptionKeyDown(event, filter, option, index, values)\n            }\n            onHover={() => {\n              this.focusRingVisible = false;\n              this.activeOptionIndex = index;\n            }}\n            onSelect={() => this.selectOption(filter, option, values)}\n          />\n        );\n      });\n    }\n\n    if (filter.kind === 'boolean') {\n      const option: FilterMenuOption = {\n        label: filter.fieldLabel ?? filter.label,\n        value: 'true',\n        description: filter.description,\n      };\n      return (\n        <ChoiceOptionRow\n          size=\"md\"\n          id={`${this.generatedId}-${filter.id}-option-0`}\n          option={{\n            label: option.label,\n            value: option.value,\n            subtext: option.description,\n          }}\n          selected={value === true}\n          active\n          focusRingVisible={this.focusRingVisible}\n          usesSubtext={Boolean(option.description)}\n          tabIndex={0}\n          leading={\n            <span\n              class=\"ds-choice-item__icon ds-control-icon-box ds-interaction-fill__content\"\n              aria-hidden=\"true\"\n            >\n              <ds-checkbox label=\"\" size=\"md\" checked={value === true} presentation />\n            </span>\n          }\n          onKeyDown={(event: KeyboardEvent) => {\n            if (event.key === 'ArrowLeft') this.handlePanelKeyDown(event, filter.id);\n            else if (event.key === 'Enter' || event.key === ' ') {\n              event.preventDefault();\n              this.dsChange.emit({ filterId: filter.id, value: value !== true });\n            }\n          }}\n          onHover={() => {\n            this.focusRingVisible = false;\n          }}\n          onSelect={() => this.dsChange.emit({ filterId: filter.id, value: value !== true })}\n        />\n      );\n    }\n\n    const dateValue = typeof value === 'string' ? value : '';\n    const inputId = `${this.generatedId}-${filter.id}-input`;\n    return (\n      <div class=\"filter-menu__date-field\">\n        <label htmlFor={inputId}>\n          <ds-text as=\"span\" variant=\"text-body-small\" emphasis color=\"primary\">\n            {filter.fieldLabel ?? 'Event date'}\n          </ds-text>\n        </label>\n        <div class=\"filter-menu__date-control ds-control-frame ds-control--md\">\n          <input\n            id={inputId}\n            class=\"filter-menu__date-input\"\n            type=\"date\"\n            value={dateValue}\n            onInput={(event: Event) =>\n              this.dsChange.emit({\n                filterId: filter.id,\n                value: (event.target as HTMLInputElement).value,\n              })\n            }\n          />\n          {dateValue ? (\n            <ds-button-unfilled\n              variant=\"icon\"\n              icon=\"CrossCircle\"\n              size=\"sm\"\n              isInset\n              hasBorder={false}\n              rounded\n              ariaLabel={`${this.clearLabel} ${filter.fieldLabel ?? filter.label}`}\n              onDsClick={(event: CustomEvent<MouseEvent>) => {\n                event.stopPropagation();\n                this.dsChange.emit({ filterId: filter.id, value: '' });\n              }}\n            />\n          ) : null}\n        </div>\n      </div>\n    );\n  }\n\n  private get captionIconOnly(): boolean {\n    return (\n      this.collapseLabel &&\n      this.captionCompact &&\n      this.usesInternalTrigger &&\n      Boolean(this.icon)\n    );\n  }\n\n  private syncCaptionCompactObserver(): void {\n    this.disconnectCaptionCompactObserver();\n    if (!this.collapseLabel) {\n      if (this.captionCompact) this.captionCompact = false;\n      return;\n    }\n    this.captionCompactDisconnect = observeTableCaptionCompact(this.el, compact => {\n      if (this.captionCompact !== compact) this.captionCompact = compact;\n    });\n  }\n\n  private disconnectCaptionCompactObserver(): void {\n    this.captionCompactDisconnect?.();\n    this.captionCompactDisconnect = undefined;\n  }\n\n  render() {\n    const state =\n      this.closing && this.closingSnapshot\n        ? this.closingSnapshot\n        : {\n            filters: this.filters,\n            values: this.values,\n            activeFilterId: this.activeFilterId,\n          };\n    const activeFilter = this.selectedFilter(state.filters, state.activeFilterId);\n    const totalSelected = this.totalSelected(state.filters, state.values);\n    const activeFilterCount = this.activeFilterCount(state.filters, state.values);\n    const hasActiveFilters = activeFilterCount > 0;\n    const label = `${this.triggerLabel}${hasActiveFilters ? ` · ${activeFilterCount}` : ''}`;\n    const textVariant = CONTROL_TEXT_VARIANT[this.size];\n    const iconSize = ICON_SIZE[this.size];\n    const popupId = `${this.generatedId}-popup`;\n    const popupStyle: Record<string, string> = {\n      position: 'fixed',\n      left: '0',\n      top: '0',\n      width: this.menuWidth,\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    return (\n      <Host\n        class={{\n          'filter-menu-host': true,\n          'filter-menu-host--external-trigger': !this.usesInternalTrigger,\n          'ds-select-trigger-host': this.usesInternalTrigger,\n          [`ds-control--${this.size}`]: true,\n          'ds-table-caption-control': this.collapseLabel,\n          'ds-table-caption-control--compact': this.captionIconOnly,\n          ...controlWidthClass(this.width),\n        }}\n      >\n        {this.usesInternalTrigger ? (\n          <button\n            ref={element => {\n              this.triggerElement = (element as HTMLButtonElement) ?? null;\n            }}\n            id={this.inputId ?? `${this.generatedId}-trigger`}\n            type=\"button\"\n            class={{\n              trigger: true,\n              'ds-control-frame': true,\n              'ds-focus-ring-inset': true,\n              'ds-interaction-fill': true,\n              'ds-interaction-fill--selected': this.activeFill && hasActiveFilters,\n              'trigger--expanded': this.open || this.closing,\n              'trigger--bordered': this.hasBorder,\n              'trigger--has-value': hasActiveFilters,\n              [`ds-control--${this.size}`]: true,\n            }}\n            role=\"combobox\"\n            aria-haspopup=\"dialog\"\n            aria-expanded={String(this.open)}\n            aria-controls={this.open ? popupId : undefined}\n            aria-label={this.ariaLabel ?? this.menuLabel}\n            onClick={(event: MouseEvent) => this.toggleFromTrigger(event)}\n            onKeyDown={(event: KeyboardEvent) => this.handleTriggerKeyDown(event)}\n          >\n            {this.icon ? (\n              <span\n                class=\"trigger__prefix ds-control-icon-box ds-interaction-fill__content\"\n                aria-hidden=\"true\"\n              >\n                <ds-icon name={this.icon} size={iconSize} color=\"inherit\" />\n              </span>\n            ) : null}\n            {this.captionIconOnly ? null : (\n              <ds-text\n                class=\"trigger__label-box trigger__label trigger__label-content ds-control-label-box ds-interaction-fill__content\"\n                as=\"span\"\n                variant={textVariant}\n                color=\"inherit\"\n                lineTruncation={1}\n              >\n                {label}\n              </ds-text>\n            )}\n            {this.captionIconOnly ? null : (\n              <span\n                class=\"trigger__chevron ds-control-icon-box ds-interaction-fill__content\"\n                aria-hidden=\"true\"\n              >\n                <ds-icon name=\"ChevronDown\" size={iconSize} color=\"inherit\" />\n              </span>\n            )}\n          </button>\n        ) : null}\n\n        {this.shouldRender ? (\n          <div\n            id={popupId}\n            popover=\"manual\"\n            class={{\n              'filter-menu-popup': true,\n              'ds-choice-popup': true,\n              'ds-choice-popup--closing': this.closing,\n            }}\n            style={popupStyle}\n            role=\"dialog\"\n            aria-label={this.menuLabel}\n          >\n            {activeFilter ? (\n              <div class=\"filter-menu\">\n                <div class=\"filter-menu__body\">\n                  <div\n                    class=\"filter-menu__categories ds-choice-list ds-chrome-column ds-chrome-space--sm\"\n                    role=\"tablist\"\n                    aria-label={this.categoriesLabel}\n                    aria-orientation=\"vertical\"\n                  >\n                    {state.filters.map(filter => {\n                      const active = filter.id === activeFilter.id;\n                      const count = this.selectedCount(filter, state.values);\n                      return (\n                        <div\n                          key={filter.id}\n                          class={{\n                            'filter-menu__category-section': true,\n                            'ds-choice-section--divided': Boolean(filter.divider),\n                          }}\n                        >\n                          <button\n                            id={`${this.generatedId}-${filter.id}-tab`}\n                            type=\"button\"\n                            role=\"tab\"\n                            data-filter-category={filter.id}\n                            class={{\n                              'filter-menu__category': true,\n                              'filter-menu__category--active': active,\n                              'ds-choice-item': true,\n                              'ds-control-frame': true,\n                              'ds-control--md': true,\n                              'ds-focus-ring-inset': true,\n                              'ds-focus-ring--visible': active && this.focusRingVisible,\n                              'ds-interaction-fill': true,\n                              'ds-interaction-fill--selected': active,\n                            }}\n                            aria-selected={String(active)}\n                            aria-controls={`${this.generatedId}-panel`}\n                            tabIndex={active ? 0 : -1}\n                            onMouseDown={() => {\n                              this.focusRingVisible = false;\n                            }}\n                            onClick={() => this.selectCategory(filter.id)}\n                            onKeyDown={(event: KeyboardEvent) =>\n                              this.handleCategoryKeyDown(event, state.filters, filter.id)\n                            }\n                          >\n                            <ds-text\n                              class=\"ds-choice-item__content ds-choice-item__label ds-control-label-box ds-interaction-fill__content\"\n                              as=\"span\"\n                              variant=\"text-body-medium\"\n                              color={active ? 'primary' : 'secondary'}\n                            >\n                              {filter.label}\n                            </ds-text>\n                            {count > 0 ? (\n                              <ds-tag\n                                class=\"ds-choice-item__tag ds-interaction-fill__content\"\n                                label={String(count)}\n                                size=\"sm\"\n                                intent=\"brand\"\n                                contrast=\"bold\"\n                                isInset\n                                rounded\n                              />\n                            ) : null}\n                          </button>\n                        </div>\n                      );\n                    })}\n                  </div>\n\n                  <div\n                    id={`${this.generatedId}-panel`}\n                    class=\"filter-menu__options\"\n                    role=\"tabpanel\"\n                    aria-labelledby={`${this.generatedId}-${activeFilter.id}-tab`}\n                  >\n                    <div\n                      key={activeFilter.id}\n                      class=\"filter-menu__option-list ds-choice-list ds-chrome-column ds-chrome-space--sm\"\n                      role=\"listbox\"\n                      aria-label={activeFilter.label}\n                      aria-multiselectable={activeFilter.kind === 'multiple' ? 'true' : undefined}\n                    >\n                      {this.renderOptions(activeFilter, state.values)}\n                    </div>\n                  </div>\n                </div>\n\n                {totalSelected > 0 ? (\n                  <div class=\"filter-menu__footer ds-choice-footer\">\n                    <div class=\"ds-choice-footer__content ds-control--md\">\n                      <ds-text\n                        class=\"ds-choice-footer__summary\"\n                        as=\"span\"\n                        variant=\"text-body-medium\"\n                        color=\"secondary\"\n                        aria-live=\"polite\"\n                      >\n                        {totalSelected} selected\n                      </ds-text>\n                      <button\n                        class=\"filter-menu__clear ds-choice-footer__clear ds-text-action\"\n                        type=\"button\"\n                        onClick={() => this.dsClear.emit()}\n                      >\n                        <ds-text as=\"span\" variant=\"text-body-medium\" color=\"inherit\">\n                          {this.clearLabel}\n                        </ds-text>\n                      </button>\n                    </div>\n                  </div>\n                ) : null}\n              </div>\n            ) : null}\n          </div>\n        ) : null}\n      </Host>\n    );\n  }\n}\n",
      "type": "registry:ui"
    }
  ]
}
