{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "button-unfilled",
  "title": "ButtonUnfilled",
  "description": "Secondary command and compact chrome button with optional persistent toggle state, surface-aware interaction treatment, and async progress.",
  "type": "registry:ui",
  "meta": {
    "library": "@ds-mo/ui",
    "distribution": "npm",
    "storybook": "https://zainadeel.github.io/compomo/?path=/story/button-unfilled",
    "source": "src/wc/components/ButtonUnfilled/ButtonUnfilled.tsx",
    "intentStatus": "complete",
    "intent": {
      "audience": "general",
      "status": "stable",
      "summary": "Secondary command and compact chrome button with optional persistent toggle state, surface-aware interaction treatment, and async progress.",
      "useWhen": [
        "A secondary command accompanies the single filled primary action in a local decision area.",
        "A toolbar, tool rail, overflow area, or shell region needs a compact icon action.",
        "A command represents a persistent toggle or mode whose active state remains meaningful after activation.",
        "A row, card, or toolbar needs an overflow or more-options menu; this is the only component that carries that role."
      ],
      "avoidWhen": [
        "The command is the single primary action in a local decision area; use ButtonFilled instead.",
        "The control selects an application destination or local content panel; use the matching navigation component.",
        "Active styling would only indicate the most recently invoked command or add decorative emphasis.",
        "Surface context would be used to make the action louder or quieter rather than describe its actual parent surface."
      ],
      "alternatives": [
        {
          "when": "The command is the single primary action in the decision area.",
          "component": "component:ds-button-filled",
          "reason": "ButtonFilled owns highest-emphasis commands."
        },
        {
          "when": "The choices are persistent application destinations.",
          "component": "component:ds-panel-nav",
          "reason": "PanelNav owns primary route selection and navigation semantics."
        },
        {
          "when": "The choices are transient commands or contextual options.",
          "component": "component:ds-menu",
          "reason": "Menu owns temporary popup collections and their keyboard model."
        }
      ],
      "commonlyComposedWith": [
        "component:ds-button-filled",
        "component:ds-menu",
        "component:ds-panel-tools"
      ],
      "patterns": [
        "pattern:menu-trigger"
      ],
      "accessibility": [
        "Provide an explicit accessible name for every icon-only action.",
        "Set hasMenu when the button triggers a Menu; it implies aria-haspopup of menu. Pass controls with the Menu id and expanded from the same open state given to Menu. Expanded popup triggers hold only the pressed wash while open, without changing their resting foreground; expanded disclosure controls without popup semantics retain the normal resting appearance.",
        "Give an icon-only menu button an accessible name that states the role, such as More options or Customize table, because no visible label or chevron carries it.",
        "Use haspopup directly for a non-menu popup such as a listbox or dialog; those cases get no chevron.",
        "Use pressed only for a genuine toggle button; it supplies aria-pressed and dsChange intent. Use isActive only for owner-controlled visual emphasis inside a composite whose selection semantics live on the owner.",
        "Treat notification dots as supplemental unread or status information; never use a dot as the sole state, meaning, or accessible name.",
        "Loading keeps the action focused and named, exposes busy and disabled state, and blocks activation; announce broader operation progress from the owning workflow when needed.",
        "Use inactive state only when an unavailable action must remain visible.",
        "For form submission, use native submit behavior and handle the form submit event rather than submitting from the button click event."
      ],
      "states": [
        "Every size applies one complete control-density recipe across frame height, inline padding, icon zone, text-container inset, gap, and radius.",
        "Set isInset only when the button is nested inside a same-size control. The default single inset removes 4px overall; insetDepth double removes 8px overall so the parent can add 4px per edge while preserving its outer alignment box. Double inset falls back to single at xs to protect minimum icon geometry.",
        "A true pressed toggle and owner-controlled isActive emphasis share the selected visual treatment, but only pressed supplies toggle semantics and dsChange.",
        "General content UI keeps the selected interaction fill and border for pressed or active emphasis.",
        "Shell, navigation, and tool-rail composites use isActive with primary foreground across the complete control and no selected fill or border.",
        "The omitted default surface treatment is for primary and secondary parent surfaces and uses the brand-active selected fill; pass faint explicitly on faint surfaces to use the neutral active fill.",
        "Surface context must match the actual parent surface; translucent, inverted, media, and always-dark treatments are reserved for those surfaces.",
        "Loading prevents duplicate activation without applying inactive opacity or removing the current keyboard focus.",
        "Rounded changes only the control radius to the half-radius treatment; it does not change hierarchy, size, variant, or interaction semantics.",
        "Physical press scaling is on by default. Disable it only when an owning composite must keep child or background geometry fixed; hover and pressed interaction washes remain available.",
        "Icon and icon-label loading replace the icon in place. Label-only loading centers the loader while preserving the button's measured width.",
        "hasMenu covers two distinct menu-button shapes and the variant selects between them. With label or icon-label the action has a menu, and a trailing chevron carries the affordance. With icon the button is a menu and no chevron is added.",
        "Because the icon-only shape has no chevron, its glyph must convey the menu on its own. Use Ellipses for a generic more-options or overflow control. Use a specific icon when the menu has a named purpose, such as Table for Customize table.",
        "An open popup is a transient pressed state rather than a selected one. While expanded is true and haspopup or hasMenu identifies a popup, the pressed wash is held for the popup's complete rendered lifecycle and survives hover without promoting foreground or selected fill, including when chrome opts out of selected fills with activeFill set to false. Expanded disclosure buttons without popup semantics remain visually neutral."
      ],
      "responsiveBehavior": [
        "The consuming layout decides when an icon-label action becomes icon-only and must provide an explicit accessible name after changing variants. Table caption Customize is one such layout: below 900px it uses the icon shape, and a customized catalog promotes the Table icon to primary.",
        "When collapseLabel is set inside a named ds-table container narrower than 900px, an icon-label caption action omits its label and chevron and becomes icon-only while keeping its accessible name. An active control then promotes the icon to primary.",
        "Use fill width only when the parent layout requires a full-width secondary action."
      ],
      "frameworkCaveats": {
        "customElements": [
          "For form submission, use the native submit type and listen to the form submit event; use dsClick for non-submit commands.",
          "Roving-focus composites own the button's tab-stop placement and selection semantics."
        ],
        "react": [
          "Use pressed for controlled toggles and isActive for composite-owned emphasis; keep async state synchronized from the owner."
        ],
        "angular": [
          "Use pressed for controlled toggles and isActive for composite-owned emphasis; keep async state synchronized from the owner."
        ]
      },
      "references": [
        {
          "label": "Storybook examples",
          "path": "src/wc/components/ButtonUnfilled/ButtonUnfilled.stories.ts"
        }
      ]
    },
    "api": {
      "props": {
        "activeFill": {
          "type": "boolean",
          "resolvedType": "boolean",
          "attribute": "active-fill",
          "default": "true",
          "required": false,
          "mutable": false,
          "description": "When active, render the selected interaction fill.\nDefault `true` for general UI. Shell chrome (nav / tool rails) should pass\n`false` so selection is foreground-only (primary color, no fill)."
        },
        "ariaLabel": {
          "type": "string | null",
          "resolvedType": "null | string",
          "attribute": "aria-label",
          "default": "null",
          "required": false,
          "mutable": false,
          "description": "Accessible name override. Required for icon-only buttons."
        },
        "background": {
          "type": "ButtonUnfilledBackground | undefined",
          "resolvedType": "\"always-dark\" | \"bold\" | \"faint\" | \"inverted\" | \"media\" | \"medium\" | \"strong\" | \"translucent\" | undefined",
          "attribute": "background",
          "required": false,
          "mutable": false,
          "description": "Actual parent surface context. Omit on primary and secondary surfaces."
        },
        "collapseLabel": {
          "type": "boolean",
          "resolvedType": "boolean",
          "attribute": "collapse-label",
          "default": "false",
          "required": false,
          "mutable": false,
          "description": "Collapse the visible label and chevron to an icon-only control when the\nowning `ds-table` caption is narrower than 900px. The trigger omits those\nparts rather than clipping them."
        },
        "controls": {
          "type": "string | undefined",
          "resolvedType": "string | undefined",
          "attribute": "controls",
          "required": false,
          "mutable": false
        },
        "dot": {
          "type": "boolean",
          "resolvedType": "boolean",
          "attribute": "dot",
          "default": "false",
          "required": false,
          "mutable": false,
          "description": "Show a notification dot at the top-right of the icon zone (icon variant only)."
        },
        "expanded": {
          "type": "boolean | undefined",
          "resolvedType": "boolean | undefined",
          "attribute": "expanded",
          "required": false,
          "mutable": false,
          "description": "Controlled disclosure or popup state forwarded to `aria-expanded`.\nPopup triggers hold only the pressed wash while open; their resting\nforeground is unchanged. Ordinary disclosures remain visually neutral."
        },
        "focusTabIndex": {
          "type": "number",
          "resolvedType": "number | undefined",
          "attribute": "tab-index",
          "required": false,
          "mutable": false,
          "description": "Native `tabindex` for roving keyboard groups in shell chrome.\nOmit for the default button tab stop (`0`)."
        },
        "hasBorder": {
          "type": "boolean",
          "resolvedType": "boolean",
          "attribute": "has-border",
          "default": "true",
          "required": false,
          "mutable": false,
          "description": "Show a 1px secondary inset border. Default on; shell chrome can pass `false`."
        },
        "hasMenu": {
          "type": "boolean",
          "resolvedType": "boolean",
          "attribute": "has-menu",
          "default": "false",
          "required": false,
          "mutable": false,
          "description": "Mark the button as a Menu trigger. Implies `aria-haspopup=\"menu\"` and covers\nboth menu-button shapes:\n\n- `label` / `icon-label` — the action *has* a menu; a trailing chevron carries\n  the affordance.\n- `icon` — the button *is* a menu. No chevron is added, so the glyph must\n  convey the menu on its own. Use `Ellipses` for generic more-options;\n  use a specific icon when the menu has a named purpose, such as\n  `Table` for Customize table.\n\nUse `haspopup` directly for non-menu popups."
        },
        "haspopup": {
          "type": "ButtonUnfilledPopup | undefined",
          "resolvedType": "\"dialog\" | \"grid\" | \"listbox\" | \"menu\" | \"tree\" | \"true\" | undefined",
          "attribute": "haspopup",
          "required": false,
          "mutable": false
        },
        "icon": {
          "type": "string",
          "resolvedType": "string",
          "attribute": "icon",
          "default": "''",
          "required": false,
          "mutable": false,
          "description": "Icon name passed to <ds-icon> for `icon` / `icon-label` variants."
        },
        "insetDepth": {
          "type": "ControlInsetDepth",
          "resolvedType": "\"double\" | \"single\"",
          "attribute": "inset-depth",
          "default": "'single'",
          "required": false,
          "mutable": false,
          "description": "Single removes 4px overall; double removes 8px overall (xs stays single)."
        },
        "isActive": {
          "type": "boolean",
          "resolvedType": "boolean",
          "attribute": "is-active",
          "default": "false",
          "required": false,
          "mutable": false,
          "description": "Owner-controlled visual emphasis inside a composite. This does not add\ntoggle semantics and does not emit dsChange."
        },
        "isInactive": {
          "type": "boolean",
          "resolvedType": "boolean",
          "attribute": "is-inactive",
          "default": "false",
          "required": false,
          "mutable": false,
          "description": "Disables interaction."
        },
        "isInset": {
          "type": "boolean",
          "resolvedType": "boolean",
          "attribute": "is-inset",
          "default": "false",
          "required": false,
          "mutable": false,
          "description": "Use reduced outer geometry when nested inside a control of the same size."
        },
        "isLoading": {
          "type": "boolean",
          "resolvedType": "boolean",
          "attribute": "is-loading",
          "default": "false",
          "required": false,
          "mutable": false,
          "description": "Shows an inline loader and prevents interaction without applying inactive opacity."
        },
        "label": {
          "type": "string",
          "resolvedType": "string",
          "attribute": "label",
          "default": "''",
          "required": false,
          "mutable": false,
          "description": "Visible text for `label` / `icon-label` variants."
        },
        "pressScale": {
          "type": "boolean",
          "resolvedType": "boolean",
          "attribute": "press-scale",
          "default": "true",
          "required": false,
          "mutable": false,
          "description": "Scale down during a physical pointer press.\nDisable when an owning composite requires fixed child or background geometry."
        },
        "pressed": {
          "type": "boolean | undefined",
          "resolvedType": "boolean | undefined",
          "attribute": "pressed",
          "required": false,
          "mutable": false,
          "description": "Controlled state for a genuine toggle button. Adds aria-pressed, promotes\nactive styling, and makes activation emit dsChange with the requested state."
        },
        "rounded": {
          "type": "boolean",
          "resolvedType": "boolean",
          "attribute": "rounded",
          "default": "false",
          "required": false,
          "mutable": false,
          "description": "Use the half-radius treatment instead of the default control radius."
        },
        "size": {
          "type": "ButtonUnfilledSize",
          "resolvedType": "\"lg\" | \"md\" | \"sm\" | \"xs\"",
          "attribute": "size",
          "default": "'md'",
          "required": false,
          "mutable": false,
          "description": "Control density (height, padding, icon, type)."
        },
        "type": {
          "type": "'button' | 'submit' | 'reset'",
          "resolvedType": "\"button\" | \"reset\" | \"submit\"",
          "attribute": "type",
          "default": "'button'",
          "required": false,
          "mutable": false,
          "description": "Native button type."
        },
        "variant": {
          "type": "ButtonUnfilledVariant",
          "resolvedType": "\"icon\" | \"icon-label\" | \"label\"",
          "attribute": "variant",
          "default": "'label'",
          "required": false,
          "mutable": false,
          "description": "Content layout. Default is label-only; pass `icon` for icon-only chrome\n(nav / tool rails) or `icon-label` for leading icon + text."
        },
        "width": {
          "type": "ButtonUnfilledWidth",
          "resolvedType": "\"fill\" | \"hug\"",
          "attribute": "width",
          "default": "'hug'",
          "required": false,
          "mutable": false,
          "description": "Width fit — hug content (default) or fill the parent."
        }
      },
      "events": [
        {
          "name": "dsChange",
          "detail": "boolean",
          "bubbles": true,
          "cancelable": true,
          "composed": true
        },
        {
          "name": "dsClick",
          "detail": "MouseEvent",
          "bubbles": true,
          "cancelable": true,
          "composed": true
        }
      ],
      "methods": [
        {
          "name": "setFocus",
          "signature": "setFocus() => Promise<void>"
        }
      ],
      "slots": []
    },
    "props": {
      "activeFill": {
        "type": "boolean",
        "resolvedType": "boolean",
        "attribute": "active-fill",
        "default": "true",
        "required": false,
        "mutable": false,
        "description": "When active, render the selected interaction fill.\nDefault `true` for general UI. Shell chrome (nav / tool rails) should pass\n`false` so selection is foreground-only (primary color, no fill)."
      },
      "ariaLabel": {
        "type": "string | null",
        "resolvedType": "null | string",
        "attribute": "aria-label",
        "default": "null",
        "required": false,
        "mutable": false,
        "description": "Accessible name override. Required for icon-only buttons."
      },
      "background": {
        "type": "ButtonUnfilledBackground | undefined",
        "resolvedType": "\"always-dark\" | \"bold\" | \"faint\" | \"inverted\" | \"media\" | \"medium\" | \"strong\" | \"translucent\" | undefined",
        "attribute": "background",
        "required": false,
        "mutable": false,
        "description": "Actual parent surface context. Omit on primary and secondary surfaces."
      },
      "collapseLabel": {
        "type": "boolean",
        "resolvedType": "boolean",
        "attribute": "collapse-label",
        "default": "false",
        "required": false,
        "mutable": false,
        "description": "Collapse the visible label and chevron to an icon-only control when the\nowning `ds-table` caption is narrower than 900px. The trigger omits those\nparts rather than clipping them."
      },
      "controls": {
        "type": "string | undefined",
        "resolvedType": "string | undefined",
        "attribute": "controls",
        "required": false,
        "mutable": false
      },
      "dot": {
        "type": "boolean",
        "resolvedType": "boolean",
        "attribute": "dot",
        "default": "false",
        "required": false,
        "mutable": false,
        "description": "Show a notification dot at the top-right of the icon zone (icon variant only)."
      },
      "expanded": {
        "type": "boolean | undefined",
        "resolvedType": "boolean | undefined",
        "attribute": "expanded",
        "required": false,
        "mutable": false,
        "description": "Controlled disclosure or popup state forwarded to `aria-expanded`.\nPopup triggers hold only the pressed wash while open; their resting\nforeground is unchanged. Ordinary disclosures remain visually neutral."
      },
      "focusTabIndex": {
        "type": "number",
        "resolvedType": "number | undefined",
        "attribute": "tab-index",
        "required": false,
        "mutable": false,
        "description": "Native `tabindex` for roving keyboard groups in shell chrome.\nOmit for the default button tab stop (`0`)."
      },
      "hasBorder": {
        "type": "boolean",
        "resolvedType": "boolean",
        "attribute": "has-border",
        "default": "true",
        "required": false,
        "mutable": false,
        "description": "Show a 1px secondary inset border. Default on; shell chrome can pass `false`."
      },
      "hasMenu": {
        "type": "boolean",
        "resolvedType": "boolean",
        "attribute": "has-menu",
        "default": "false",
        "required": false,
        "mutable": false,
        "description": "Mark the button as a Menu trigger. Implies `aria-haspopup=\"menu\"` and covers\nboth menu-button shapes:\n\n- `label` / `icon-label` — the action *has* a menu; a trailing chevron carries\n  the affordance.\n- `icon` — the button *is* a menu. No chevron is added, so the glyph must\n  convey the menu on its own. Use `Ellipses` for generic more-options;\n  use a specific icon when the menu has a named purpose, such as\n  `Table` for Customize table.\n\nUse `haspopup` directly for non-menu popups."
      },
      "haspopup": {
        "type": "ButtonUnfilledPopup | undefined",
        "resolvedType": "\"dialog\" | \"grid\" | \"listbox\" | \"menu\" | \"tree\" | \"true\" | undefined",
        "attribute": "haspopup",
        "required": false,
        "mutable": false
      },
      "icon": {
        "type": "string",
        "resolvedType": "string",
        "attribute": "icon",
        "default": "''",
        "required": false,
        "mutable": false,
        "description": "Icon name passed to <ds-icon> for `icon` / `icon-label` variants."
      },
      "insetDepth": {
        "type": "ControlInsetDepth",
        "resolvedType": "\"double\" | \"single\"",
        "attribute": "inset-depth",
        "default": "'single'",
        "required": false,
        "mutable": false,
        "description": "Single removes 4px overall; double removes 8px overall (xs stays single)."
      },
      "isActive": {
        "type": "boolean",
        "resolvedType": "boolean",
        "attribute": "is-active",
        "default": "false",
        "required": false,
        "mutable": false,
        "description": "Owner-controlled visual emphasis inside a composite. This does not add\ntoggle semantics and does not emit dsChange."
      },
      "isInactive": {
        "type": "boolean",
        "resolvedType": "boolean",
        "attribute": "is-inactive",
        "default": "false",
        "required": false,
        "mutable": false,
        "description": "Disables interaction."
      },
      "isInset": {
        "type": "boolean",
        "resolvedType": "boolean",
        "attribute": "is-inset",
        "default": "false",
        "required": false,
        "mutable": false,
        "description": "Use reduced outer geometry when nested inside a control of the same size."
      },
      "isLoading": {
        "type": "boolean",
        "resolvedType": "boolean",
        "attribute": "is-loading",
        "default": "false",
        "required": false,
        "mutable": false,
        "description": "Shows an inline loader and prevents interaction without applying inactive opacity."
      },
      "label": {
        "type": "string",
        "resolvedType": "string",
        "attribute": "label",
        "default": "''",
        "required": false,
        "mutable": false,
        "description": "Visible text for `label` / `icon-label` variants."
      },
      "pressScale": {
        "type": "boolean",
        "resolvedType": "boolean",
        "attribute": "press-scale",
        "default": "true",
        "required": false,
        "mutable": false,
        "description": "Scale down during a physical pointer press.\nDisable when an owning composite requires fixed child or background geometry."
      },
      "pressed": {
        "type": "boolean | undefined",
        "resolvedType": "boolean | undefined",
        "attribute": "pressed",
        "required": false,
        "mutable": false,
        "description": "Controlled state for a genuine toggle button. Adds aria-pressed, promotes\nactive styling, and makes activation emit dsChange with the requested state."
      },
      "rounded": {
        "type": "boolean",
        "resolvedType": "boolean",
        "attribute": "rounded",
        "default": "false",
        "required": false,
        "mutable": false,
        "description": "Use the half-radius treatment instead of the default control radius."
      },
      "size": {
        "type": "ButtonUnfilledSize",
        "resolvedType": "\"lg\" | \"md\" | \"sm\" | \"xs\"",
        "attribute": "size",
        "default": "'md'",
        "required": false,
        "mutable": false,
        "description": "Control density (height, padding, icon, type)."
      },
      "type": {
        "type": "'button' | 'submit' | 'reset'",
        "resolvedType": "\"button\" | \"reset\" | \"submit\"",
        "attribute": "type",
        "default": "'button'",
        "required": false,
        "mutable": false,
        "description": "Native button type."
      },
      "variant": {
        "type": "ButtonUnfilledVariant",
        "resolvedType": "\"icon\" | \"icon-label\" | \"label\"",
        "attribute": "variant",
        "default": "'label'",
        "required": false,
        "mutable": false,
        "description": "Content layout. Default is label-only; pass `icon` for icon-only chrome\n(nav / tool rails) or `icon-label` for leading icon + text."
      },
      "width": {
        "type": "ButtonUnfilledWidth",
        "resolvedType": "\"fill\" | \"hug\"",
        "attribute": "width",
        "default": "'hug'",
        "required": false,
        "mutable": false,
        "description": "Width fit — hug content (default) or fill the parent."
      }
    },
    "events": [
      {
        "name": "dsChange",
        "detail": "boolean",
        "bubbles": true,
        "cancelable": true,
        "composed": true
      },
      {
        "name": "dsClick",
        "detail": "MouseEvent",
        "bubbles": true,
        "cancelable": true,
        "composed": true
      }
    ],
    "methods": [
      {
        "name": "setFocus",
        "signature": "setFocus() => Promise<void>"
      }
    ],
    "slots": [],
    "exports": {
      "customElement": "ds-button-unfilled",
      "react": "DsButtonUnfilled",
      "vue": "DsButtonUnfilled",
      "angular": "DsButtonUnfilled"
    },
    "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-button-unfilled.js';",
        "example": "<ds-button-unfilled></ds-button-unfilled>"
      },
      "react": {
        "import": "import { DsButtonUnfilled } from '@ds-mo/ui/react';",
        "example": "<DsButtonUnfilled />"
      },
      "vue": {
        "import": "import { DsButtonUnfilled } from '@ds-mo/ui/vue';",
        "example": "<DsButtonUnfilled />"
      },
      "angular": {
        "import": "import { DsButtonUnfilled } from '@ds-mo/ui/angular/ds-button-unfilled';",
        "example": "<ds-button-unfilled></ds-button-unfilled>"
      },
      "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",
    "loader",
    "text"
  ],
  "files": [
    {
      "path": "src/wc/components/ButtonUnfilled/ButtonUnfilled.css",
      "content": "@import '../../utils/button-base.css';\n@import '../../utils/table-caption-control.css';\n\n:host {\n  --ds-button-unfilled-fg: var(--color-foreground-secondary);\n  --ds-button-unfilled-fg-active: var(--color-foreground-primary);\n  --ds-button-unfilled-dot-ring: var(--color-background-secondary);\n  --ds-button-unfilled-border: var(--color-border-secondary);\n}\n\n.button-unfilled {\n  background: transparent;\n  color: var(--ds-button-unfilled-fg);\n  /* Hover/press/selected fill: `.ds-interaction-fill` (+ `--selected` / `--on-*`).\n     Active foreground changes remain independent from popup-open pressed paint. */\n}\n\n.button-unfilled--active {\n  color: var(--ds-button-unfilled-fg-active);\n}\n\n/*\n * An open popup is a transient pressed state, not a selected state. Keep the\n * pressed wash visible for the popup's complete rendered lifecycle without\n * promoting the resting foreground or selected fill.\n */\n/* Inset stroke via interaction-fill ::after (above selected / hover washes). */\n.button-unfilled--bordered {\n  --ds-interaction-border-color: var(--ds-button-unfilled-border);\n}\n\n/* Inactive: `.ds-control-inactive` (50% opacity). */\n\n/* Foreground / chrome tokens per surface — interaction tokens come from util `--on-*`. */\n.button-unfilled--background-medium {\n  --ds-button-unfilled-fg: var(--color-foreground-on-medium-background-secondary);\n  --ds-button-unfilled-fg-active: var(--color-foreground-on-medium-background-primary);\n  --ds-button-unfilled-dot-ring: var(--color-background-medium-neutral);\n  --ds-button-unfilled-border: var(--color-border-on-medium-background-secondary);\n}\n\n.button-unfilled--background-bold {\n  --ds-button-unfilled-fg: var(--color-foreground-on-bold-background-secondary);\n  --ds-button-unfilled-fg-active: var(--color-foreground-on-bold-background-primary);\n  --ds-button-unfilled-dot-ring: var(--color-background-bold-neutral);\n  --ds-button-unfilled-border: var(--color-border-on-bold-background-secondary);\n}\n\n.button-unfilled--background-strong {\n  --ds-button-unfilled-fg: var(--color-foreground-on-strong-background-secondary);\n  --ds-button-unfilled-fg-active: var(--color-foreground-on-strong-background-primary);\n  --ds-button-unfilled-dot-ring: var(--color-background-strong-neutral);\n  --ds-button-unfilled-border: var(--color-border-on-strong-background-secondary);\n}\n\n.button-unfilled--background-translucent {\n  --ds-button-unfilled-fg: var(--color-translucent-foreground-secondary);\n  --ds-button-unfilled-fg-active: var(--color-translucent-foreground-primary);\n  --ds-button-unfilled-dot-ring: var(--color-translucent-translucent);\n  --ds-button-unfilled-border: var(--color-translucent-border-secondary);\n}\n\n.button-unfilled--background-inverted {\n  --ds-button-unfilled-fg: var(--color-inverted-foreground-secondary);\n  --ds-button-unfilled-fg-active: var(--color-inverted-foreground-primary);\n  --ds-button-unfilled-dot-ring: var(--color-inverted-background);\n  --ds-button-unfilled-border: var(--color-inverted-border-secondary);\n}\n\n.button-unfilled--background-media {\n  --ds-button-unfilled-fg: var(--color-media-foreground-secondary);\n  --ds-button-unfilled-fg-active: var(--color-media-foreground-primary);\n  --ds-button-unfilled-dot-ring: var(--color-media-background);\n  --ds-button-unfilled-border: var(--color-media-border-secondary);\n}\n\n.button-unfilled--on-always-dark {\n  --ds-button-unfilled-fg: var(--color-always-dark-foreground-secondary);\n  --ds-button-unfilled-fg-active: var(--color-always-dark-foreground-primary);\n  --ds-button-unfilled-dot-ring: var(--color-always-dark-background);\n  --ds-button-unfilled-border: var(--color-always-dark-border-secondary);\n}\n\n/*\n * After surface ring defaults: selected fill remaps the knock-out to the active\n * wash so the badge halo matches the selected background.\n */\n.button-unfilled.ds-interaction-fill--selected {\n  --ds-button-unfilled-dot-ring: var(--ds-interaction-active);\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "src/wc/components/ButtonUnfilled/ButtonUnfilled.tsx",
      "content": "import { Component, Element, Event, EventEmitter, h, Host, Method, Prop, State, Watch } from '@stencil/core';\nimport { controlWidthClass } from '../../utils';\nimport { beginElevatedControlPress } from '../../utils/control-press';\nimport { renderButtonContent } from '../../utils/button-render';\nimport type { ControlInsetDepth } from '../../utils/control-text';\nimport { observeTableCaptionCompact } from '../../utils/table-caption-compact';\nimport type {\n  ButtonPopup,\n  ButtonSize,\n  ButtonVariant,\n  ButtonWidth,\n} from '../../utils/button-types';\n\nexport type ButtonUnfilledBackground =\n  | 'faint'\n  | 'medium'\n  | 'bold'\n  | 'strong'\n  | 'translucent'\n  | 'inverted'\n  | 'media'\n  | 'always-dark';\n\nexport type ButtonUnfilledVariant = ButtonVariant;\nexport type ButtonUnfilledSize = ButtonSize;\nexport type ButtonUnfilledWidth = ButtonWidth;\nexport type ButtonUnfilledPopup = ButtonPopup;\n\n@Component({\n  tag: 'ds-button-unfilled',\n  styleUrl: 'ButtonUnfilled.css',\n  scoped: true,\n})\nexport class ButtonUnfilled {\n  @Element() el!: HTMLElement;\n\n  /**\n   * Content layout. Default is label-only; pass `icon` for icon-only chrome\n   * (nav / tool rails) or `icon-label` for leading icon + text.\n   */\n  @Prop() variant: ButtonUnfilledVariant = 'label';\n\n  /** Control density (height, padding, icon, type). */\n  @Prop() size: ButtonUnfilledSize = 'md';\n\n  /** Use reduced outer geometry when nested inside a control of the same size. */\n  @Prop() isInset: boolean = false;\n\n  /** Single removes 4px overall; double removes 8px overall (xs stays single). */\n  @Prop() insetDepth: ControlInsetDepth = 'single';\n\n  /** Width fit — hug content (default) or fill the parent. */\n  @Prop() width: ButtonUnfilledWidth = 'hug';\n\n  /** Visible text for `label` / `icon-label` variants. */\n  @Prop() label: string = '';\n\n  /** Icon name passed to <ds-icon> for `icon` / `icon-label` variants. */\n  @Prop() icon: string = '';\n\n  /**\n   * Owner-controlled visual emphasis inside a composite. This does not add\n   * toggle semantics and does not emit dsChange.\n   */\n  @Prop() isActive: boolean = false;\n\n  /**\n   * When active, render the selected interaction fill.\n   * Default `true` for general UI. Shell chrome (nav / tool rails) should pass\n   * `false` so selection is foreground-only (primary color, no fill).\n   */\n  @Prop() activeFill: boolean = true;\n\n  /** Show a 1px secondary inset border. Default on; shell chrome can pass `false`. */\n  @Prop() hasBorder: boolean = true;\n\n  /** Use the half-radius treatment instead of the default control radius. */\n  @Prop() rounded: boolean = false;\n\n  /**\n   * Scale down during a physical pointer press.\n   * Disable when an owning composite requires fixed child or background geometry.\n   */\n  @Prop() pressScale: boolean = true;\n\n  /** Show a notification dot at the top-right of the icon zone (icon variant only). */\n  @Prop() dot: boolean = false;\n\n  /** Disables interaction. */\n  @Prop() isInactive: boolean = false;\n\n  /** Shows an inline loader and prevents interaction without applying inactive opacity. */\n  @Prop() isLoading: boolean = false;\n\n  /** Native button type. */\n  @Prop() type: 'button' | 'submit' | 'reset' = 'button';\n\n  /** Actual parent surface context. Omit on primary and secondary surfaces. */\n  @Prop() background: ButtonUnfilledBackground | undefined;\n\n  /** Accessible name override. Required for icon-only buttons. */\n  @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n  @Prop() controls: string | undefined;\n  /**\n   * Controlled disclosure or popup state forwarded to `aria-expanded`.\n   * Popup triggers hold only the pressed wash while open; their resting\n   * foreground is unchanged. Ordinary disclosures remain visually neutral.\n   */\n  @Prop() expanded: boolean | undefined;\n  @Prop() haspopup: ButtonUnfilledPopup | undefined;\n  /**\n   * Controlled state for a genuine toggle button. Adds aria-pressed, promotes\n   * active styling, and makes activation emit dsChange with the requested state.\n   */\n  @Prop() pressed: boolean | undefined;\n\n  /**\n   * Collapse the visible label and chevron to an icon-only control when the\n   * owning `ds-table` caption is narrower than 900px. The trigger omits those\n   * parts rather than clipping them.\n   */\n  @Prop({ reflect: true }) collapseLabel: boolean = false;\n\n  /**\n   * Mark the button as a Menu trigger. Implies `aria-haspopup=\"menu\"` and covers\n   * both menu-button shapes:\n   *\n   * - `label` / `icon-label` — the action *has* a menu; a trailing chevron carries\n   *   the affordance.\n   * - `icon` — the button *is* a menu. No chevron is added, so the glyph must\n   *   convey the menu on its own. Use `Ellipses` for generic more-options;\n   *   use a specific icon when the menu has a named purpose, such as\n   *   `Table` for Customize table.\n   *\n   * Use `haspopup` directly for non-menu popups.\n   */\n  @Prop() hasMenu: boolean = false;\n\n  /**\n   * Native `tabindex` for roving keyboard groups in shell chrome.\n   * Omit for the default button tab stop (`0`).\n   */\n  @Prop({ attribute: 'tab-index' }) focusTabIndex?: number;\n\n  @Event() dsClick!: EventEmitter<MouseEvent>;\n  @Event() dsChange!: EventEmitter<boolean>;\n\n  @State() private captionCompact = false;\n\n  private buttonEl: HTMLButtonElement | null = null;\n  private captionCompactDisconnect: (() => void) | undefined;\n  private hasLoaded = false;\n\n  componentDidLoad(): void {\n    this.hasLoaded = true;\n    this.syncCaptionCompactObserver();\n  }\n\n  connectedCallback(): void {\n    if (this.hasLoaded) this.syncCaptionCompactObserver();\n  }\n\n  disconnectedCallback(): void {\n    this.disconnectCaptionCompactObserver();\n  }\n\n  @Watch('collapseLabel')\n  onCollapseLabelChange() {\n    this.syncCaptionCompactObserver();\n  }\n\n  @Method()\n  async setFocus() {\n    this.buttonEl?.focus();\n  }\n\n  private handleClick = (event: MouseEvent) => {\n    if (this.isInactive || this.isLoading) {\n      event.preventDefault();\n      event.stopPropagation();\n      return;\n    }\n    this.dsClick.emit(event);\n    if (this.pressed !== undefined) this.dsChange.emit(!this.pressed);\n  };\n\n  private get captionIconOnly(): boolean {\n    return (\n      this.collapseLabel &&\n      this.captionCompact &&\n      this.variant === 'icon-label' &&\n      Boolean(this.icon)\n    );\n  }\n\n  private get visualVariant(): ButtonUnfilledVariant {\n    return this.captionIconOnly ? 'icon' : this.variant;\n  }\n\n  private get showDot(): boolean {\n    return this.variant === 'icon' && this.dot && !this.isLoading;\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  private get resolvedHaspopup(): ButtonUnfilledPopup | undefined {\n    return this.haspopup ?? (this.hasMenu ? 'menu' : undefined);\n  }\n\n  private get doubleInset(): boolean {\n    return this.isInset && this.insetDepth === 'double' && this.size !== 'xs';\n  }\n\n  /** Only popup triggers hold a pressed visual while expanded. */\n  private get expandedPopup(): boolean {\n    return this.expanded === true && this.resolvedHaspopup !== undefined && !this.isInactive;\n  }\n\n  private get visuallyActive(): boolean {\n    return this.isActive || this.pressed === true;\n  }\n\n  /** Knock-out ring: selected fill → active wash; otherwise surface token. */\n  private get dotRing(): string {\n    if ((this.isActive || this.pressed === true) && this.activeFill) {\n      return 'var(--ds-interaction-active)';\n    }\n    return 'var(--ds-button-unfilled-dot-ring)';\n  }\n\n  private get accessibleName(): string | undefined {\n    if (this.ariaLabel) return this.ariaLabel;\n    if (this.isLoading && this.variant === 'label' && this.label) return this.label;\n    return undefined;\n  }\n\n  render() {\n    const bg = this.background;\n    const cls: Record<string, boolean> = {\n      'button-unfilled': true,\n      'ds-button': true,\n      'ds-focus-ring-inset': true,\n      'ds-control-press-scale': this.pressScale,\n      'ds-interaction-fill': true,\n      'ds-interaction-fill--selected':\n        (this.isActive || this.pressed === true) &&\n        this.activeFill &&\n        !this.isInactive,\n      'ds-interaction-fill--on-faint': bg === 'faint',\n      'ds-interaction-fill--on-medium': bg === 'medium',\n      'ds-interaction-fill--on-bold': bg === 'bold',\n      'ds-interaction-fill--on-strong': bg === 'strong',\n      'ds-interaction-fill--on-translucent': bg === 'translucent',\n      'ds-interaction-fill--on-inverted': bg === 'inverted',\n      'ds-interaction-fill--on-media': bg === 'media',\n      'ds-interaction-fill--on-always-dark': bg === 'always-dark',\n      'button-unfilled--active': this.visuallyActive,\n      'button-unfilled--expanded': this.expandedPopup,\n      'ds-button--expanded': this.expandedPopup,\n      'button-unfilled--bordered': this.hasBorder,\n      'ds-button--bordered': this.hasBorder,\n      'button-unfilled--rounded': this.rounded,\n      'ds-button--rounded': this.rounded,\n      'ds-control-inactive': this.isInactive,\n      'ds-control--lg': this.size === 'lg',\n      'ds-control--md': this.size === 'md',\n      'ds-control--sm': this.size === 'sm',\n      'ds-control--xs': this.size === 'xs',\n      'ds-control--inset': this.isInset && !this.doubleInset,\n      'ds-control--inset-double': this.doubleInset,\n      'ds-control-frame': true,\n      'button-unfilled--icon': this.visualVariant === 'icon',\n      'ds-button--icon': this.visualVariant === 'icon',\n      'button-unfilled--label': this.visualVariant === 'label',\n      'button-unfilled--icon-label': this.visualVariant === 'icon-label',\n      'button-unfilled--background-faint': bg === 'faint',\n      'button-unfilled--background-medium': bg === 'medium',\n      'button-unfilled--background-bold': bg === 'bold',\n      'button-unfilled--background-strong': bg === 'strong',\n      'button-unfilled--background-translucent': bg === 'translucent',\n      'button-unfilled--background-inverted': bg === 'inverted',\n      'button-unfilled--background-media': bg === 'media',\n      'button-unfilled--on-always-dark': bg === 'always-dark',\n    };\n\n    return (\n      <Host\n        class={{\n          'button-unfilled-host': true,\n          'button-unfilled-host--icon': this.visualVariant === 'icon',\n          'ds-button-host--icon': this.visualVariant === 'icon',\n          'ds-control--lg': this.size === 'lg',\n          'ds-control--md': this.size === 'md',\n          'ds-control--sm': this.size === 'sm',\n          'ds-control--xs': this.size === 'xs',\n          'ds-control--inset': this.isInset && !this.doubleInset,\n          'ds-control--inset-double': this.doubleInset,\n          'ds-table-caption-control': this.collapseLabel,\n          'ds-table-caption-control--compact': this.captionIconOnly,\n          ...controlWidthClass(this.width),\n        }}\n        tabIndex={-1}\n      >\n        <button\n          ref={el => {\n            this.buttonEl = el ?? null;\n          }}\n          type={this.type}\n          class={cls}\n          disabled={this.isInactive}\n          tabIndex={this.focusTabIndex ?? 0}\n          aria-label={this.accessibleName}\n          aria-busy={this.isLoading ? 'true' : undefined}\n          aria-disabled={this.isLoading ? 'true' : undefined}\n          aria-controls={this.controls}\n          aria-expanded={this.expanded === undefined ? undefined : String(this.expanded)}\n          aria-haspopup={this.resolvedHaspopup}\n          aria-pressed={this.pressed === undefined ? undefined : String(this.pressed)}\n          onPointerDown={event =>\n            beginElevatedControlPress(\n              event,\n              this.pressScale && !this.isInactive && !this.isLoading,\n            )\n          }\n          onClick={this.handleClick}\n        >\n          {renderButtonContent({\n            namespace: 'button-unfilled',\n            variant: this.visualVariant,\n            size: this.size,\n            label: this.label,\n            icon: this.icon,\n            hasMenu: this.hasMenu,\n            isLoading: this.isLoading,\n            dot: {\n              visible: this.showDot,\n              background: this.dotRing,\n            },\n          })}\n        </button>\n      </Host>\n    );\n  }\n}\n",
      "type": "registry:ui"
    }
  ]
}
