{
  "name": "Combobox",
  "type": "component",
  "description": "A text field that filters a list and selects one value from it. Portalled and theme-aware.",
  "export": "Combobox",
  "path": "@gtmi/strix-react/atoms/combobox",
  "figma": null,
  "figmaAbsent": "Base UI primitive. Figma draws no combobox; `.input-dropdown` is `Select`, a different control.",
  "props": [
    {
      "name": "id",
      "type": "string",
      "required": true,
      "description": "Associates the label and the help text."
    },
    {
      "name": "options",
      "type": "ComboboxOption[] | ((searchTerm: string, signal: AbortSignal) => ComboboxOption[] | Promise<ComboboxOption[]>)",
      "description": "Static array, or an async loader. The function form selects async mode; there is no separate flag to keep in sync with it. In async mode the server filters, so Base UI’s own filter is switched off."
    },
    {
      "name": "label",
      "type": "ReactNode",
      "description": "Visible label. Use aria-label when the label lives elsewhere."
    },
    {
      "name": "helpText",
      "type": "ReactNode",
      "description": "Hint below the field. Replaced by errorText in the error state."
    },
    {
      "name": "errorText",
      "type": "ReactNode",
      "description": "Shown in the error state and announced when it appears."
    },
    {
      "name": "value",
      "type": "string",
      "description": "Controlled value."
    },
    {
      "name": "defaultValue",
      "type": "string"
    },
    {
      "name": "onValueChange",
      "type": "(value: string) => void",
      "description": "Fires with the chosen value, or '' when cleared."
    },
    {
      "name": "autocomplete",
      "type": "boolean",
      "default": "true",
      "description": "With false the list stays whole and the field behaves as a Select trigger."
    },
    {
      "name": "groupBy",
      "type": "string",
      "description": "Group options by this key on each option."
    },
    {
      "name": "state",
      "type": "completed | error | success | warning",
      "description": "Matches Input and Select. error also sets aria-invalid."
    },
    {
      "name": "emptyState",
      "type": "ReactNode",
      "default": "No results"
    },
    {
      "name": "className",
      "type": "string",
      "description": "Applied to the input."
    },
    {
      "name": "popupClassName",
      "type": "string",
      "description": "Applied to the popup."
    },
    {
      "name": "fetchPage",
      "type": "(params: { limit: number; offset: number; pageToken?: string; searchTerm: string }, signal: AbortSignal) => InfiniteOptionsPage | Promise<InfiniteOptionsPage>",
      "description": "Paginated loader for lists too long to send at once. Loads more as the popup scrolls. Mutually exclusive with a function-form options; options wins if both are given."
    },
    {
      "name": "pageSize",
      "type": "number",
      "default": "100",
      "description": "Items requested per page in fetchPage mode"
    },
    {
      "name": "debounceMs",
      "type": "number",
      "default": "250",
      "description": "Debounce applied to typed input before an async load fires"
    },
    {
      "name": "allowFreeForm",
      "type": "boolean",
      "default": "false",
      "description": "Let the user commit a value that is not in the list. A create row appears while the typed text matches no option’s label"
    },
    {
      "name": "initialCustomValues",
      "type": "string[]",
      "description": "Custom values already committed, typically restored form state. Read once on mount"
    },
    {
      "name": "createLabel",
      "type": "(value: string) => string",
      "default": "(value) => `Create \"${value}\"`",
      "description": "Label for the create row"
    },
    {
      "name": "errorState",
      "type": "ReactNode",
      "default": "Unable to load options",
      "description": "Shown in place of the list when an async load fails"
    },
    {
      "name": "loadingLabel",
      "type": "string",
      "default": "Loading options",
      "description": "Announced politely through Base UI’s Status element while options load"
    }
  ],
  "ai_tags": ["combobox", "autocomplete", "filter", "form", "portal", "primitive"],
  "since": "0.2.0",
  "tokens_used": [
    "--color-surface-secondary",
    "--color-surface-base",
    "--color-surface-hover",
    "--color-surface-accent-extra-soft",
    "--color-surface-disabled",
    "--color-border-accent",
    "--color-border-error",
    "--color-border-success",
    "--color-border-warning",
    "--color-text-primary",
    "--color-text-secondary",
    "--color-text-tertiary",
    "--color-text-disabled",
    "--color-text-disabled-on-surface",
    "--color-icon-secondary",
    "--color-icon-accent",
    "--shadow-md",
    "--radius-sm",
    "--radius-md",
    "--z-index-popover",
    "--animate-popup-in",
    "--animate-popup-out"
  ],
  "notes": "Async option loaders, paginated infinite scroll and free-form \"create X\" entries are all implemented (Phase 3c Task 5b). Mode is derived from which prop is given, never stored: a function-form `options` selects async, `fetchPage` selects paginated, and `allowFreeForm` layers over any source. About a third of maker-ds’s hand-rolled machinery was deliberately NOT ported because Base UI 1.6 already provides it: controlled `inputValue`/`onInputValueChange` instead of hand-managed search state, and `Combobox.Status` for the polite load announcement. Base UI documents that Status must stay mounted for screen readers to announce changes consistently, so the loading condition is on its children; maker-ds conditionally renders the whole element, and a test here asserts the element survives. Four upstream defects were fixed rather than ported: no request cancellation (superseded loads now abort their `AbortSignal`, where upstream only ignores the late result and leaves the connection open), no stale-guard or mutex on the first-page load so pages could interleave, a mount effect keyed on `items.length` that refetched unfiltered after a non-empty load followed by an empty one, and `console.error` with no `error` state a consumer could render. Also dropped: `enableRefetchOnChange` (implemented upstream, passed by neither combobox), result caching (upstream has none, and adding it would invent invalidation semantics), `resolve-display-text`’s object-value branches and `isItemEqualToValue` (Strix values are strings), and `helper-components` (depends on Badge and Typography fields `ComboboxOption` does not have). Client-side filtering is switched off in any remote mode, because the server already filtered and re-filtering would hide a fuzzy or synonym match the server judged valid; it is also off under `allowFreeForm`, or the create row’s own label would be filtered out by the text that produced it. PORTALLED: stamps the active theme on the popup. Note role=\"listbox\" sits on Base UI's inner List, not on the popup, so a test looking for the themed element must query the popup by data-slot. Base UI treats whatever is in `items` as the item value, so this passes an array of value strings rather than the option objects - passing objects would hand every callback and filter an object where the API promises a string. Filtering compares the option's *label*: Base UI's default filter compares the value, so typing \"aus\" against { value: 'au', label: 'Australia' } matched nothing. The list renders through a function child, which renders the filtered collection; mapping the original array would render every option regardless of the query and only look filtered by accident. The error message replaces the hint rather than stacking under it.",
  "parts": [
    {
      "name": "combobox-status",
      "description": "Base UI Status element, always mounted so screen readers announce load changes consistently"
    },
    {
      "name": "combobox-error",
      "description": "Shown in place of the list when an async load fails"
    }
  ]
}
