{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "combobox",
  "type": "registry:ui",
  "title": "Combobox",
  "description": "Fluent 2-styled filterable select with shadcn-style composable APIs. Built on @base-ui/react's Combobox for portalled positioning, focus management, keyboard navigation and built-in filtering; the field matches the kit's Input (bottom brand-accent focus, Clear + chevron), the flyout uses Fluent shadow-16 elevation, and selected/highlighted items use CheckmarkRegular + brand-neutral accent states. Passes Base UI's `multiple` prop through for a future Multi Select.",
  "author": "graundtech <https://github.com/graundtech/fluent2-react-kit>",
  "dependencies": [
    "@base-ui/react",
    "@fluentui/react-icons"
  ],
  "registryDependencies": [
    "https://fluent2-react-kit.graund.io/r/utils.json"
  ],
  "files": [
    {
      "path": "components/ui/combobox.tsx",
      "type": "registry:ui",
      "target": "components/ui/combobox.tsx",
      "content": "\"use client\";\n\nimport { Combobox as ComboboxPrimitive } from \"@base-ui/react/combobox\";\nimport {\n  CheckmarkRegular,\n  ChevronDownRegular,\n  DismissRegular,\n} from \"@fluentui/react-icons\";\nimport type { ComponentProps } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Combobox — Fluent 2-styled, composable filterable select built on Base UI.\n *\n * This is the REFERENCE for the kit's filter-list family: Multi Select and\n * Command Menu are built by later agents reading THIS file the way the overlay\n * batch read `select.tsx`. Read the divergence notes and extension-point notes\n * below before extending it.\n *\n * It is the typed-into sibling of `Select`: the field is the kit's Input recipe\n * (h-8, `border-input` + `border-b-stroke-accessible`, bottom brand-accent\n * focus) instead of a button trigger, and the flyout is `SelectContent`'s\n * validated popup recipe verbatim (`bg-popover`, `border`, `shadow-16`,\n * `rounded-md`, `min-w-[var(--anchor-width)]`, scale+fade motion on Base UI's\n * `data-starting-style`/`data-ending-style` hooks). Item rows are `SelectItem`'s\n * 32px `data-[highlighted]` rows with the right-side check.\n *\n * ## Why `@base-ui/react/combobox` and NOT `@base-ui/react/autocomplete`\n * Base UI ships BOTH. They share the same AriaCombobox engine but draw the line\n * at *selection*:\n * - **Combobox** has a selection model — `value`/`onValueChange` (the committed\n *   selected value, single or `multiple`) that is SEPARATE from\n *   `inputValue`/`onInputValueChange` (the transient filter text). Picking an\n *   item commits a discrete value; the input filters the list. This is exactly a\n *   \"filterable select\".\n * - **Autocomplete** is `selectionMode: 'none'` — it deliberately omits\n *   `selectedValue`/`onValueChange`; the input value IS the only state. It's for\n *   free-text-with-suggestions (search boxes), where there is no discrete\n *   committed choice.\n * The whole filter-list family (this, Multi Select, Command Menu) selects\n * discrete items, so it wraps **combobox**. A future free-text search field is\n * the one case that would wrap autocomplete instead.\n *\n * ## Base UI mapping (conventions §9)\n * Namespace import of `@base-ui/react/combobox`, matching the export shape in\n * node_modules (like `select.tsx`/`avatar.tsx`). shadcn-style part names mapped\n * onto Base UI's model:\n *\n * | Exported (kit name) | Base UI primitive                                          |\n * | ------------------- | ---------------------------------------------------------- |\n * | `Combobox`          | `Combobox.Root`                                            |\n * | `ComboboxInput`     | field wrapper + `Combobox.Input` + `Combobox.Clear` + `Combobox.Trigger` (+ `Combobox.Icon`) |\n * | `ComboboxContent`   | `Combobox.Portal` + `Combobox.Positioner` + `Combobox.Popup` |\n * | `ComboboxList`      | `Combobox.List`  ← see divergence 5                        |\n * | `ComboboxEmpty`     | `Combobox.Empty`                                           |\n * | `ComboboxItem`      | `Combobox.Item` (+ `Combobox.ItemIndicator`)              |\n * | `ComboboxGroup`     | `Combobox.Group`                                          |\n * | `ComboboxLabel`     | `Combobox.GroupLabel`  ← see divergence 1                 |\n * | `ComboboxSeparator` | `Combobox.Separator`                                      |\n * | `ComboboxValue`     | `Combobox.Value`  ← see divergence 4                      |\n *\n * ## Divergences vs Base UI naming (all deliberate)\n * 1. **`ComboboxLabel` maps to `Combobox.GroupLabel`, not `Combobox.Label`.**\n *    Base UI has two label parts: `Combobox.Label` labels the whole control (a\n *    field `<label>`), `Combobox.GroupLabel` is the heading inside a\n *    `Combobox.Group`. Like Select, the kit exposes the *group* heading as\n *    `ComboboxLabel` and does NOT re-export the control-level label — pair\n *    `ComboboxInput` with the kit's own `Label` component (a separate registry\n *    item; install it explicitly). Labeling recipe: give `Label` an `id` and set\n *    `aria-labelledby` on the `ComboboxInput` — but unlike Select's button\n *    trigger, the input IS a labelable form field, so a plain `htmlFor`/`id`\n *    pairing works too (the preview uses `htmlFor` + `id`).\n * 2. **`ComboboxInput` is a composed field, not a bare `<input>`.** It renders a\n *    field-chrome wrapper (`data-slot=\"combobox-input-wrapper\"`) around the real\n *    `Combobox.Input` (`data-slot=\"combobox-input\"`), plus a `Combobox.Clear`\n *    reset button (Base UI auto-unmounts it when the field is empty) and a\n *    `Combobox.Trigger` chevron that toggles the popup. This is the sibling of\n *    `SelectTrigger`, which likewise bakes its chevron in. `className`, `ref` and\n *    all other props forward to the inner `<input>`; the wrapper is styled via\n *    `wrapperClassName`. The chevron/clear are `tabIndex={-1}` (not extra tab\n *    stops — the input already owns `role=\"combobox\"` + keyboard control).\n * 3. **No `Combobox.Arrow`, no scroll buttons.** The popup is a flyout beneath\n *    the field (not an anchored caret-overlap like a native `<select>`), so the\n *    positioner arrow and Select's ScrollUp/DownArrow parts are omitted; the\n *    `List` scrolls with `overflow-y-auto`.\n * 4. **`ComboboxValue` renders the raw selected value unless `items` is given.**\n *    Same model as Select's `SelectValue`: `Combobox.Value` shows the raw\n *    `value` unless `<Combobox items={[{ value, label }]}>` is supplied or a\n *    render child is used. It is exported for callers who display the selection\n *    outside the input; the field itself shows the selection as input text (Base\n *    UI fills the input on select), so most single-select UIs never need it.\n * 5. **`ComboboxList` is a separate part (Select auto-wraps its `List`).** Base\n *    UI does the filtering, so the natural API passes `items` to `<Combobox>` and\n *    renders the list with a render-function child:\n *    `<ComboboxList>{(item) => <ComboboxItem value={item}>{item.label}</ComboboxItem>}</ComboboxList>`.\n *    `ComboboxEmpty` is a sibling of `ComboboxList` INSIDE `ComboboxContent` (not\n *    a `List` child) — it mirrors Base UI's own structure and lets the no-match\n *    caption sit at the popup level. Static `ComboboxItem` children also work,\n *    but the built-in filter only runs against the `items` prop, so filtering UIs\n *    should pass `items` + the render-function form. Unlike `SelectContent`\n *    (which owns its `List`), keeping `List` explicit is what makes correct\n *    `Empty` placement and the render-function API possible.\n *\n * ## Divergences vs shadcn's Combobox recipe\n * shadcn has NO Combobox component — its docs \"Combobox\" is a hand-rolled recipe\n * composing `Popover` + `Command` (cmdk) with local `useState`, `open`, and a\n * manually-wired `CommandInput`/`CommandItem`/`onSelect`. This kit instead ships\n * a real `Combobox*` part family:\n * - **Filtering is built in.** Base UI's `Combobox.Root` filters `List` items\n *   against the input value automatically (`mode=\"list\"` default); the shadcn\n *   recipe leans on cmdk's `Command` to filter. No `cmdk` dependency here.\n * - **One selection model, not two hooks.** shadcn juggles Popover `open` +\n *   Command `value` + an external `value` `useState`. Here `value`/`inputValue`\n *   are one model on the Root — controlled or uncontrolled — exposed honestly.\n * - **Composable parts, not a monolith.** `Combobox`, `ComboboxInput`,\n *   `ComboboxContent`, `ComboboxItem`, … read as the sibling of `Select`, not as\n *   a bespoke Popover+Command assembly the consumer re-wires each time.\n *\n * ## Extension points for Multi Select (do NOT style chips here)\n * `multiple` is passed through UNBLOCKED (`Combobox`'s generic second type param,\n * exactly like `Select`). When `multiple` is set, Base UI:\n * - makes `value`/`onValueChange` arrays;\n * - keeps `Combobox.Clear` clearing to `[]`;\n * - exposes `Combobox.Chips` / `Combobox.Chip` / `Combobox.ChipRemove` for a\n *   token field, and `Combobox.Row` for grid layouts.\n * The Multi Select agent should build its OWN field part (e.g. `MultiSelectInput`\n * / a chips field) that wraps `Combobox.Chips` around `Combobox.Input` inside the\n * same field-chrome wrapper this file uses — it must NOT reuse `ComboboxInput`,\n * which is the single-line surface. This file intentionally leaves chip styling\n * unspecified; the `combobox-input-wrapper` class string is the shared field\n * recipe to copy. Everything else (`ComboboxContent`, `ComboboxItem`,\n * `ComboboxGroup`, `ComboboxLabel`, `ComboboxEmpty`) is reusable as-is.\n *\n * ## `\"use client\"` — required\n * `@fluentui/react-icons` forces it: the package's shared icon-sizing module\n * (`createFluentIcon.styles.js`) calls `@griffel/react`'s `__styles()` at module\n * scope without its own `'use client'`, so importing an icon into a Server\n * Component makes `next build` (Turbopack) fail collecting page data. Same fix\n * and root cause as `select.tsx`/`checkbox.tsx` (conventions §9). Every Base UI\n * Combobox part module already carries its own `'use client'`.\n *\n * ## data-slot note\n * `Combobox` (Root) renders no DOM element and its Base UI props type is strict\n * (no `data-*` passthrough), so it carries no `data-slot`. Every part that\n * renders an element does (`combobox-input`, `combobox-content`, `combobox-item`,\n * …) — the styling/testing hook consumers rely on.\n */\n\nfunction Combobox<Value, Multiple extends boolean | undefined = false>(\n  props: ComboboxPrimitive.Root.Props<Value, Multiple>\n) {\n  return <ComboboxPrimitive.Root {...props} />;\n}\n\n/**\n * Value — displays the selected value (Base UI `Combobox.Value`). Renders the\n * raw value unless `items` is passed to `<Combobox>` or a render child is used\n * (divergence 4). Exported for showing the selection outside the input; the\n * field itself already reflects the selection as input text.\n */\nfunction ComboboxValue(props: ComponentProps<typeof ComboboxPrimitive.Value>) {\n  return <ComboboxPrimitive.Value data-slot=\"combobox-value\" {...props} />;\n}\n\n/**\n * Input — the Fluent single-line combobox field (divergence 2). A field-chrome\n * wrapper holds the real `<input>` (transparent, fills the field), a `Clear`\n * reset button (auto-unmounts when empty) and a chevron `Trigger`. The wrapper\n * carries the kit Input recipe: h-8, `rounded-md`, `border-input` +\n * `border-b-stroke-accessible` at rest, and the Fluent bottom brand accent on\n * focus via an inset box-shadow (no reflow) — keyed off `has-[input:focus-visible]`\n * because focus lands on the inner input, not the wrapper. `aria-invalid` on the\n * input swaps the field to destructive, declared after focus so it wins.\n * `className`/`ref`/`...props` forward to the inner `<input>`; style the wrapper\n * with `wrapperClassName`.\n */\nfunction ComboboxInput({\n  className,\n  wrapperClassName,\n  ...props\n}: ComponentProps<typeof ComboboxPrimitive.Input> & {\n  wrapperClassName?: string;\n}) {\n  return (\n    <div\n      data-slot=\"combobox-input-wrapper\"\n      className={cn(\n        // layout — Fluent medium field, 32px, matches Input. Resting bottom edge\n        // uses the darker NeutralStrokeAccessible (#616161) accent; the other\n        // sides stay border-input. Focus (border-primary + inset underline) and\n        // aria-invalid (border-destructive) both override it.\n        \"flex h-8 w-full items-center rounded-md border border-input border-b-stroke-accessible bg-background text-sm\",\n        // motion — color + box-shadow so the focus accent animates (§4)\n        \"transition-[color,box-shadow] duration-fast ease-ease\",\n        // focus — Fluent bottom brand accent via inset box-shadow, no reflow (§4).\n        // has-[input:focus-visible] because focus is on the inner input; text\n        // inputs match :focus-visible on pointer focus too, so this covers both.\n        \"has-[input:focus-visible]:border-primary has-[input:focus-visible]:shadow-[inset_0_-2px_0_0_var(--brand-80)] dark:has-[input:focus-visible]:shadow-[inset_0_-2px_0_0_var(--brand-100)]\",\n        // invalid — shadcn aria-invalid treatment; after focus so it wins the border\n        \"has-[input[aria-invalid='true']]:border-destructive has-[input[aria-invalid='true']]:ring-destructive/20 dark:has-[input[aria-invalid='true']]:ring-destructive/40\",\n        \"has-[input[aria-invalid='true']:focus-visible]:shadow-[inset_0_-2px_0_0_var(--destructive)]\",\n        // disabled — opacity read when the whole control is disabled\n        \"has-[input:disabled]:pointer-events-none has-[input:disabled]:opacity-50\",\n        wrapperClassName\n      )}\n    >\n      <ComboboxPrimitive.Input\n        data-slot=\"combobox-input\"\n        className={cn(\n          \"h-full min-w-0 flex-1 bg-transparent px-3 py-1 outline-none\",\n          \"placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground\",\n          className\n        )}\n        {...props}\n      />\n      <ComboboxPrimitive.Clear\n        data-slot=\"combobox-clear\"\n        aria-label=\"Clear\"\n        tabIndex={-1}\n        className={cn(\n          \"mr-0.5 flex size-6 shrink-0 items-center justify-center rounded-sm text-muted-foreground outline-none\",\n          \"transition-colors duration-fast ease-ease hover:text-foreground\",\n          \"focus-visible:ring-2 focus-visible:ring-ring\",\n          \"[&_svg]:pointer-events-none [&_svg]:size-4\"\n        )}\n      >\n        <DismissRegular />\n      </ComboboxPrimitive.Clear>\n      <ComboboxPrimitive.Trigger\n        data-slot=\"combobox-trigger\"\n        aria-label=\"Show options\"\n        tabIndex={-1}\n        className={cn(\n          \"mr-2 flex shrink-0 items-center text-muted-foreground outline-none\",\n          \"[&_svg]:pointer-events-none [&_svg]:size-4\"\n        )}\n      >\n        <ComboboxPrimitive.Icon data-slot=\"combobox-icon\">\n          <ChevronDownRegular />\n        </ComboboxPrimitive.Icon>\n      </ComboboxPrimitive.Trigger>\n    </div>\n  );\n}\n\n/**\n * Content — the floating flyout: `Portal` → `Positioner` → `Popup`. Surface is\n * `SelectContent`'s validated recipe verbatim: `bg-popover` + `border` +\n * `shadow-16` (Fluent flyout elevation), `rounded-md`, min-width matched to the\n * field via Base UI's `--anchor-width`, and the scale+fade open/close animation\n * on `data-starting-style`/`data-ending-style` with token durations/easings.\n * `transition-[opacity,scale]` (never transform-based — conventions §3.5).\n * Children are placed directly in the popup: a `ComboboxEmpty` (no-match\n * caption) and a `ComboboxList` (the scroll container). See divergence 5.\n */\nfunction ComboboxContent({\n  className,\n  children,\n  sideOffset = 4,\n  align = \"start\",\n  side = \"bottom\",\n  ...props\n}: ComponentProps<typeof ComboboxPrimitive.Popup> &\n  Pick<\n    ComponentProps<typeof ComboboxPrimitive.Positioner>,\n    \"side\" | \"align\" | \"sideOffset\"\n  >) {\n  return (\n    <ComboboxPrimitive.Portal>\n      <ComboboxPrimitive.Positioner\n        data-slot=\"combobox-positioner\"\n        sideOffset={sideOffset}\n        align={align}\n        side={side}\n        className=\"z-50 outline-none\"\n      >\n        <ComboboxPrimitive.Popup\n          data-slot=\"combobox-content\"\n          className={cn(\n            \"relative min-w-[var(--anchor-width)] origin-[var(--transform-origin)] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-16 outline-none\",\n            // motion — subtle scale + fade on open (enter) / close (exit)\n            \"transition-[opacity,scale] duration-fast ease-decelerate-mid\",\n            \"data-starting-style:scale-95 data-starting-style:opacity-0\",\n            \"data-ending-style:scale-95 data-ending-style:opacity-0 data-ending-style:ease-accelerate-mid\",\n            className\n          )}\n          {...props}\n        >\n          {children}\n        </ComboboxPrimitive.Popup>\n      </ComboboxPrimitive.Positioner>\n    </ComboboxPrimitive.Portal>\n  );\n}\n\n/**\n * List — the scrollable options container (Base UI `Combobox.List`, divergence\n * 5). `children` may be a render function `(item, index) => …` (the filtering\n * API: pass `items` to `<Combobox>` and map each) or static `ComboboxItem`\n * children. Scrolls at `overflow-y-auto`; capped at `--available-height` or 20rem.\n */\nfunction ComboboxList({\n  className,\n  ...props\n}: ComponentProps<typeof ComboboxPrimitive.List>) {\n  return (\n    <ComboboxPrimitive.List\n      data-slot=\"combobox-list\"\n      className={cn(\n        \"max-h-[min(var(--available-height),20rem)] overflow-y-auto overscroll-contain p-1\",\n        className\n      )}\n      {...props}\n    />\n  );\n}\n\n/**\n * Empty — the no-match state (Base UI `Combobox.Empty`). Base UI renders its\n * children ONLY when the filtered list is empty, and its root element must stay\n * mounted to announce (it carries `role=\"presentation\"` + a live region), so\n * always place a `<ComboboxEmpty>` in the content rather than conditionally\n * rendering it. Muted, centered caption.\n */\nfunction ComboboxEmpty({\n  className,\n  ...props\n}: ComponentProps<typeof ComboboxPrimitive.Empty>) {\n  return (\n    <ComboboxPrimitive.Empty\n      data-slot=\"combobox-empty\"\n      className={cn(\"py-6 text-center text-sm text-muted-foreground\", className)}\n      {...props}\n    />\n  );\n}\n\n/**\n * Item — one option (Base UI `Combobox.Item`, renders a `<div>` with\n * `role=\"option\"`). `value` selects it; the selected-state check\n * (`CheckmarkRegular`) is absolutely positioned on the right inside\n * `ItemIndicator`, which Base UI only mounts for the selected item. 32px row,\n * 4px list radius, highlight (keyboard focus or hover) via `data-highlighted` →\n * `bg-accent`, disabled items muted + inert — the `SelectItem` recipe.\n */\nfunction ComboboxItem({\n  className,\n  children,\n  ...props\n}: ComponentProps<typeof ComboboxPrimitive.Item>) {\n  return (\n    <ComboboxPrimitive.Item\n      data-slot=\"combobox-item\"\n      className={cn(\n        // layout — Fluent 32px row, 4px list radius, room on the right for check\n        \"relative flex h-8 w-full cursor-default items-center rounded-md pr-8 pl-2 text-sm outline-none select-none\",\n        // rest text is NeutralForeground2, darkening to accent-foreground on\n        // highlight (Figma validation: Fluent list rows rest at #424242)\n        \"text-foreground-2 data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground\",\n        // disabled item — muted + non-interactive\n        \"data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n        // icons a consumer puts inside the item text\n        \"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className\n      )}\n      {...props}\n    >\n      <span className=\"absolute right-2 flex size-4 items-center justify-center\">\n        <ComboboxPrimitive.ItemIndicator>\n          <CheckmarkRegular className=\"size-4\" />\n        </ComboboxPrimitive.ItemIndicator>\n      </span>\n      {children}\n    </ComboboxPrimitive.Item>\n  );\n}\n\n/**\n * Group — wraps a set of items under a `ComboboxLabel` heading (Base UI\n * `Combobox.Group`). With Base UI's built-in filtering, pass grouped `items` to\n * `<Combobox>` so empty groups drop out automatically; see the preview.\n */\nfunction ComboboxGroup({\n  className,\n  ...props\n}: ComponentProps<typeof ComboboxPrimitive.Group>) {\n  return (\n    <ComboboxPrimitive.Group\n      data-slot=\"combobox-group\"\n      className={cn(className)}\n      {...props}\n    />\n  );\n}\n\n/**\n * Label — a heading for a `ComboboxGroup` (Base UI `Combobox.GroupLabel`), NOT a\n * control-level field label (divergence 1). Small muted caption, Fluent style.\n */\nfunction ComboboxLabel({\n  className,\n  ...props\n}: ComponentProps<typeof ComboboxPrimitive.GroupLabel>) {\n  return (\n    <ComboboxPrimitive.GroupLabel\n      data-slot=\"combobox-label\"\n      className={cn(\n        \"px-2 py-1.5 text-xs font-medium text-muted-foreground select-none\",\n        className\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction ComboboxSeparator({\n  className,\n  ...props\n}: ComponentProps<typeof ComboboxPrimitive.Separator>) {\n  return (\n    <ComboboxPrimitive.Separator\n      data-slot=\"combobox-separator\"\n      className={cn(\"pointer-events-none -mx-1 my-1 h-px bg-border\", className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Combobox,\n  ComboboxValue,\n  ComboboxInput,\n  ComboboxContent,\n  ComboboxList,\n  ComboboxEmpty,\n  ComboboxItem,\n  ComboboxGroup,\n  ComboboxLabel,\n  ComboboxSeparator,\n};\n"
    }
  ],
  "docs": "Base UI does the filtering, so pass `items` to `<Combobox>` and render the list with a render-function child:\n\n```tsx\n<Combobox items={fruits}>\n  <ComboboxInput placeholder=\"Search a fruit\" />\n  <ComboboxContent>\n    <ComboboxEmpty>No fruits found.</ComboboxEmpty>\n    <ComboboxList>\n      {(item) => (\n        <ComboboxItem key={item.value} value={item}>\n          {item.label}\n        </ComboboxItem>\n      )}\n    </ComboboxList>\n  </ComboboxContent>\n</Combobox>\n```\n\nBase UI's control-level `Combobox.Label` is intentionally not exposed — pair `ComboboxInput` with the kit's own `Label` item (not pulled in automatically; `combobox`'s only registryDependency is `utils`). Install it separately:\n\n```bash\nnpx shadcn@latest add <registry-url>/r/label.json\n```\n\nThe `<input>` is a labelable form field, so a plain `htmlFor`/`id` pairing works (unlike Select's button trigger). See `combobox.tsx`'s doc comment for the combobox-vs-autocomplete boundary and the Multi Select extension points."
}
