{
  "name": "Select",
  "type": "component",
  "description": "A dropdown that picks one value from a list. Portalled and theme-aware.",
  "export": "@gtmi/strix-react/atoms/select",
  "path": "@gtmi/strix-react/atoms/select",
  "figma": {
    "file": "zT3vGm4QlFEI2Jp9nxyFKI",
    "fileName": "Demo components library",
    "nodes": [
      {
        "node": "22:3967",
        "name": ".input-dropdown",
        "section": "Atoms"
      }
    ]
  },
  "props": [
    {
      "name": "options",
      "type": "SelectOption[]",
      "required": true,
      "description": "Each option: value, label, and optionally disabled."
    },
    {
      "name": "value",
      "type": "string | number",
      "description": "Controlled value."
    },
    {
      "name": "defaultValue",
      "type": "string | number",
      "description": "Uncontrolled initial value."
    },
    {
      "name": "onValueChange",
      "type": "(value) => void",
      "description": "Fires on selection. Values keep their type: pass numbers, get numbers back."
    },
    {
      "name": "placeholder",
      "type": "string",
      "default": "Select…"
    },
    {
      "name": "state",
      "type": "completed | error | success | warning",
      "description": "Matches Input's states. `error` also sets aria-invalid on the trigger."
    },
    {
      "name": "disabled",
      "type": "boolean"
    },
    {
      "name": "triggerIcon",
      "type": "ReactNode",
      "default": "<ChevronDownIcon size=\"sm\" />"
    },
    {
      "name": "className",
      "type": "string",
      "description": "Applied to the trigger."
    },
    {
      "name": "popupClassName",
      "type": "string",
      "description": "Applied to the popup."
    },
    {
      "name": "aria-label",
      "type": "string",
      "description": "Required unless the trigger is paired with a <label> or aria-labelledby."
    }
  ],
  "parts": [
    {
      "name": "SelectRoot / SelectTrigger / SelectValue / SelectIcon",
      "description": "Trigger side, for custom trigger content."
    },
    {
      "name": "SelectPortal / SelectPositioner / SelectPopup",
      "description": "The portalled list. SelectPopup carries the same theme stamp as the convenience form."
    },
    {
      "name": "SelectItem / SelectItemText / SelectItemIndicator",
      "description": "One option, with its check indicator."
    },
    {
      "name": "SelectGroup / SelectGroupLabel / SelectSeparator",
      "description": "Structure for a long or categorised list."
    }
  ],
  "ai_tags": ["select", "dropdown", "listbox", "form", "portal", "primitive"],
  "since": "0.2.0",
  "tokens_used": [
    "--color-surface-field",
    "--color-surface-field-disabled",
    "--color-surface-base",
    "--color-surface-hover",
    "--color-surface-accent-extra-soft",
    "--color-border-field-focus",
    "--color-border-field-figma",
    "--color-border-field-error",
    "--color-border-success",
    "--color-border-warning",
    "--color-border-divider-on-surface",
    "--color-text-primary",
    "--color-text-secondary",
    "--color-text-tertiary",
    "--color-text-disabled",
    "--color-text-field-description",
    "--color-icon-primary",
    "--color-icon-accent",
    "--shadow-md",
    "--radius-large",
    "--radius-md",
    "--spacing-borders-sm",
    "--spacing-borders-md",
    "--spacing-padding-horizontal-md",
    "--text-size-figma-16",
    "--leading-height-figma-16",
    "--z-index-popover",
    "--animate-popup-in",
    "--animate-popup-out"
  ],
  "notes": "PORTALLED: stamps the active theme on the popup, and a test covers the compound SelectPopup as well as the convenience form - it is easy to fix one and forget the part a caller composes by hand. Upstream's h-[56px] is gone and the separator's h-[1px] is h-px. Base UI emits null when a selection is cleared, which this wrapper drops: onValueChange is typed (value: TValue) => void, so forwarding null would be a lie to every consumer. The trigger renders the matching option's label rather than the raw value. Upstream's positioner wrote `z-popover`, a class that generated nothing - the fourth such case this phase. Use Select when the value is one of a known set; use Combobox when the list needs filtering.\n\nTHE TRIGGER IS PORTED FROM FIGMA .input-dropdown (22:3967), and it NO LONGER MATCHES Input's metrics. That was this component's stated rule -- \"a select and a text field sit side by side in forms and any difference reads as a mistake\" -- and Figma disagrees: it draws .input-text at 36px and .input-dropdown at 48px, so the difference is the design. Rule 1 makes \"Figma drew two heights\" outrank \"one height looks tidier\", and a stated frame height is per atom, so nothing shared moved. 48px as h-12, 16px horizontal padding, and radius/large 10px as rounded-large -- a token --radius-large already carried, which is why the binding's \"10 is not on Strix's radius scale\" deferral was already stale: it was true of the inherited maker-ds scale and false of the emitted layer.\n\nTHE RESTING TRIGGER STROKES NOTHING, as Figma draws it, which makes the gray-800 fill the only thing marking the control's extent at 1.39:1 against the page where WCAG 1.4.11 wants 3:1. A rule-1 conflict, shipped and recorded in docs/FIGMA-EXCEPTIONS.md; the border WIDTH is kept so the box does not move when the hover stroke appears. Hover and open both stroke color/blue/blue-200 via --color-border-field-focus, 7.58:1 on the fill it outlines and 10.50:1 against the page. That role was DERIVED from this atom for Input's focus ring, so here it is Figma's own value rather than a derivation.\n\nTHE CHEVRON FLIPS ON OPEN, and only Dev Mode shows it. The binding recorded that \"the ONLY difference between hover and active in the whole node is the 10 -> 8 radius\" and declined Figma's open state as indistinguishable from hover, which justified a compensating data-popup-open fill change. The open variant's Action Icon carries -rotate-180: Figma distinguished the two states all along, the REST extraction just could not see a transform. The fill change is dropped (Figma fills closed, hover and open identically) and the rotation is ported. The accompanying -scale-x-100 is a mirror and a no-op on a symmetrical chevron, so it is not.\n\nDISABLED RECEDES RATHER THAN LIFTING. Figma's Disabled fill is gray-850 #191f36, one step DARKER than the enabled gray-800, where --color-surface-disabled is rgba(255,255,255,0.08) and lightens. Minted as --color-surface-field-disabled, opaque -- a translucent token is filed under `unmeasurable` by the contrast helper rather than checked. Its 1.19:1 against the page and the 1.95:1 disabled label are NOT defects: WCAG 1.4.3 and 1.4.11 both exempt an inactive control.\n\nSELECT LEFT border-control.test.ts alongside Input. --color-border-control is the boundary for the controls Figma draws NO atom for -- Checkbox, Radio, Textarea, ToggleGroup -- so it is a rule-2 gap with no Figma value to conflict with, and measuring a logged 1.39:1 conflict against a 3:1 floor would assert a value this system has decided not to hold. The guard test in each block of that file is what made the removal safe.\n\nSTILL NOT PORTED: Figma drops the radius 10 -> 8 when the popup opens. That only means something against the popup's own top radius, and the popup is the unpublished .dropdown atom -- porting one half of a joint whose other half is unknown would be guessing. Figma's four states (closed, hover, active, Disabled) include none of Select's completed/error/success/warning, so all four are rule-2 gaps keeping their existing roles, and Figma shows a filled value in every variant so it says nothing about the italic placeholder either."
}
