{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "multi-select",
  "type": "registry:ui",
  "title": "Multi Select",
  "description": "Fluent 2-styled multiple-selection combobox with a chips field, built on the kit's Combobox. Selected values render as removable secondary chips in a wrapping token field (min-h-8, flex-wrap, bottom brand-accent focus); Base UI supplies multiple selection, built-in filtering, Backspace-removes-last-chip, arrow-key chip navigation, and a popup that stays open while picking. Popup-side parts (Content, List, Item, Group, Label, Empty, Separator) are re-exported from Combobox unchanged; only the chips field (MultiSelectInput, MultiSelectChip) is new.",
  "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",
    "https://fluent2-react-kit.graund.io/r/combobox.json"
  ],
  "files": [
    {
      "path": "components/ui/multi-select.tsx",
      "type": "registry:ui",
      "target": "components/ui/multi-select.tsx",
      "content": "\"use client\";\n\nimport { Combobox as ComboboxPrimitive } from \"@base-ui/react/combobox\";\nimport { DismissRegular } from \"@fluentui/react-icons\";\nimport type { ComponentProps, ReactNode } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport {\n  ComboboxContent,\n  ComboboxEmpty,\n  ComboboxGroup,\n  ComboboxItem,\n  ComboboxLabel,\n  ComboboxList,\n  ComboboxSeparator,\n} from \"./combobox\";\n\n/**\n * Multi Select — Fluent 2-styled multiple-selection combobox with a chips field.\n *\n * Built DIRECTLY on the kit's `Combobox` reference (`./combobox`). Base UI's\n * `Combobox.Root` already carries the multiple-selection engine — passing\n * `multiple` turns `value`/`onValueChange` into arrays and unlocks the chips\n * parts (`Combobox.Chips`/`Chip`/`ChipRemove`). This file's only NEW surface is\n * the **chips field** (`MultiSelectInput` + `MultiSelectChip`); everything on the\n * popup side is re-exported from `combobox.tsx` unchanged.\n *\n * ## Reuse model — what is new vs a pass-through\n * | Exported (kit name)   | Source                                              |\n * | --------------------- | --------------------------------------------------- |\n * | `MultiSelect`         | `Combobox.Root` with `multiple` defaulted to `true` |\n * | `MultiSelectInput`    | **NEW** chips field: `Combobox.Chips` (field chrome) wrapping a `Combobox.Value` chip flow + `Combobox.Input` |\n * | `MultiSelectChip`     | **NEW** one selected-value chip: `Combobox.Chip` + `Combobox.ChipRemove` |\n * | `MultiSelectContent`  | pass-through re-export of `ComboboxContent`          |\n * | `MultiSelectList`     | pass-through re-export of `ComboboxList`             |\n * | `MultiSelectItem`     | pass-through re-export of `ComboboxItem`             |\n * | `MultiSelectGroup`    | pass-through re-export of `ComboboxGroup`            |\n * | `MultiSelectLabel`    | pass-through re-export of `ComboboxLabel`            |\n * | `MultiSelectEmpty`    | pass-through re-export of `ComboboxEmpty`            |\n * | `MultiSelectSeparator`| pass-through re-export of `ComboboxSeparator`        |\n *\n * ## Divergences (all deliberate)\n * 1. **The popup parts are literal re-exports, so they keep their\n *    `data-slot=\"combobox-…\"` values** — they ARE the Combobox parts, aliased for\n *    a coherent `MultiSelect*` call site. Only the two parts this file owns get\n *    `data-slot=\"multi-select-…\"` (`multi-select-input-wrapper`,\n *    `multi-select-input`, `multi-select-chip`, `multi-select-chip-remove`). This\n *    avoids forking identical popup markup and keeps the registry item lean\n *    (`registryDependencies: [\"utils\", \"combobox\"]`, like `pagination` depends on\n *    `button`). Selected items still render the right-side `CheckmarkRegular` from\n *    `ComboboxItem` — in `multiple` mode Base UI mounts the indicator for every\n *    selected option.\n * 2. **`MultiSelectInput` is NOT `ComboboxInput`.** `ComboboxInput` is the\n *    single-line surface (h-8, one `<input>` + Clear + chevron). The chips field\n *    is a wrapping token field: `min-h-8` + `flex-wrap` + `gap-1` + `py-1` so\n *    chips reflow to multiple rows, with the text `<input>` sitting inline at the\n *    end. It reuses `combobox-input-wrapper`'s field-chrome recipe verbatim — the\n *    `border-input`/`border-b-stroke-accessible` rest edge, the\n *    `has-[input:focus-visible]` bottom brand accent, the\n *    `has-[input[aria-invalid]]` destructive form, the `has-[input:disabled]`\n *    opacity — because focus/invalid/disabled all still land on the inner input.\n * 3. **No baked-in Clear or chevron `Trigger`.** The single-select field bakes a\n *    `Combobox.Clear` + chevron in; a tags picker's affordances are the chips\n *    themselves (each removable) and Backspace-clears-last, so the chips field\n *    stays chrome-light. Open with focus + type or `ArrowDown`. Consumers who want\n *    a clear-all/chevron can compose `@base-ui/react/combobox`'s `Clear`/`Trigger`\n *    directly.\n * 4. **Chip visuals are Fluent Tag (Small, Filled), Badge-adjacent.** A chip is\n *    `bg-secondary` `text-foreground-2`, `rounded-md`, `h-6`, `px-1.5`, `text-xs` — the\n *    neutral filled look of `Badge variant=\"secondary\"` but sized for an inline\n *    token (Badge is a static 20px caption chip; a removable token needs the extra\n *    height + a trailing dismiss target). The remove button hover matches Fluent\n *    Tag's `.Secondary action` Hover (node `9112:11155`, captured in the Figma\n *    re-pass 2026-07-14): brand glyph (`--primary`) over a subtle neutral fill\n *    (`--accent`), NOT destructive-red — Fluent treats tag dismiss as a normal,\n *    reversible interactive control; the `DismissRegular` glyph carries the meaning\n *    (conventions §5). (The earlier `text-destructive` was a provisional choice\n *    kept while Pass 3's Figma read was rate-limited; the reference now adjudicates\n *    it.)\n *\n * ## Chips wiring — what Base UI provides vs what is wired here\n * The chip flow is rendered from `Combobox.Value`'s render-function child, which\n * receives the live `selectedValue` array and re-renders on every selection — so\n * an UNCONTROLLED `MultiSelect` (the preview / RSC case) shows chips without any\n * consumer `useState`. Base UI owns the keyboard model for free:\n * - **Backspace on an empty input removes the last chip** (Base UI `ComboboxInput`).\n * - **ArrowLeft/Right move between chips; Backspace/Delete on a focused chip\n *   removes it** (Base UI `ComboboxChip`).\n * - **Clicking a `ChipRemove` deselects that value and refocuses the input**\n *   (Base UI `ComboboxChipRemove`).\n * - **The popup stays OPEN after selecting** in `multiple` mode (Base UI default),\n *   so several tags can be picked in a row.\n * This file only styles those parts and maps each value to a chip.\n *\n * ## `\"use client\"` — required\n * `@fluentui/react-icons` forces it (the `DismissRegular` import): the package's\n * shared icon-sizing module calls `@griffel/react`'s `__styles()` at module scope\n * without its own directive, so importing an icon into a Server Component breaks\n * `next build` (conventions §9; same fix as `select.tsx`/`combobox.tsx`).\n */\n\nfunction MultiSelect<Value, Multiple extends boolean | undefined = true>({\n  multiple,\n  ...props\n}: ComboboxPrimitive.Root.Props<Value, Multiple>) {\n  return (\n    <ComboboxPrimitive.Root\n      multiple={(multiple ?? true) as Multiple}\n      {...props}\n    />\n  );\n}\n\n/**\n * Chip — one selected value, rendered from `MultiSelectInput`'s chip flow (or\n * composed by a caller who passes their own render child). `Combobox.Chip` holds\n * the label; the trailing `Combobox.ChipRemove` button deselects it. Base UI maps\n * a chip to `selectedValue[index]` via its composite-list order, so chips must be\n * rendered in selection order (which `Combobox.Value`'s array is).\n */\nfunction MultiSelectChip({\n  className,\n  children,\n  ...props\n}: ComponentProps<typeof ComboboxPrimitive.Chip>) {\n  return (\n    <ComboboxPrimitive.Chip\n      data-slot=\"multi-select-chip\"\n      className={cn(\n        // Fluent Tag (Small, Filled): 24px, #f5f5f5 fill, 4px radius\n        // (rounded-md), Caption-1 text in NeutralForeground2 (text-foreground-2)\n        // — exact token values from Figma validation pass 3, node 9112:10360.\n        \"inline-flex h-6 items-center gap-1 rounded-md bg-secondary px-1.5 text-xs text-foreground-2 select-none\",\n        // highlighted (chip keyboard focus) + disabled\n        \"data-[highlighted]:bg-accent data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n        className\n      )}\n      {...props}\n    >\n      {children}\n      <ComboboxPrimitive.ChipRemove\n        data-slot=\"multi-select-chip-remove\"\n        aria-label=\"Remove\"\n        className={cn(\n          \"-mr-0.5 flex size-4 shrink-0 items-center justify-center rounded-sm text-muted-foreground outline-none\",\n          // hover — Fluent Tag `.Secondary action` Hover (node 9112:11155): the\n          // dismiss glyph goes brand (`NeutralForeground2.BrandHover` #0f6cbd ==\n          // --primary) over a subtle neutral fill (`NeutralBackground3.Hover`\n          // #ebebeb ≈ --accent #f0f0f0). NOT destructive-red — removing a tag is a\n          // reversible micro-action, and Fluent treats it as a normal interactive\n          // control; the DismissRegular glyph carries the \"remove\" meaning (§5).\n          \"transition-colors duration-fast ease-ease hover:bg-accent hover:text-primary\",\n          \"focus-visible:ring-2 focus-visible:ring-ring\",\n          \"[&_svg]:pointer-events-none [&_svg]:size-3\"\n        )}\n      >\n        <DismissRegular />\n      </ComboboxPrimitive.ChipRemove>\n    </ComboboxPrimitive.Chip>\n  );\n}\n\n/**\n * Input — the Fluent chips field (divergence 2). `Combobox.Chips` is the\n * field-chrome container (flex-wrap token row) holding the chip flow and the\n * inline text `<input>`. `children`, when given, is a render function\n * `(value) => ReactNode` that maps each selected value to its chip content —\n * use it to look up a label for `{ value, label }` items or to style the chip.\n * With no `children`, each value is rendered as a `MultiSelectChip` showing\n * `String(value)` (the flat-string case). `className`/`ref`/`...props` forward to\n * the inner `<input>`; style the field with `wrapperClassName`.\n */\nfunction MultiSelectInput({\n  className,\n  wrapperClassName,\n  children,\n  ...props\n}: Omit<ComponentProps<typeof ComboboxPrimitive.Input>, \"children\"> & {\n  wrapperClassName?: string;\n  children?: (value: unknown) => ReactNode;\n}) {\n  return (\n    <ComboboxPrimitive.Chips\n      data-slot=\"multi-select-input-wrapper\"\n      className={cn(\n        // layout — wrapping token field. min-h-8 (chips reflow to rows), flex-wrap,\n        // gap-1, py-1. Resting bottom edge uses NeutralStrokeAccessible; the other\n        // sides stay border-input. Focus + aria-invalid override it (below).\n        \"flex min-h-8 w-full flex-wrap items-center gap-1 rounded-md border border-input border-b-stroke-accessible bg-background px-1.5 py-1 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.\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.Value>\n        {(selected: unknown) =>\n          (Array.isArray(selected) ? selected : []).map((value, index) =>\n            children ? (\n              children(value)\n            ) : (\n              <MultiSelectChip key={index}>{String(value)}</MultiSelectChip>\n            )\n          )\n        }\n      </ComboboxPrimitive.Value>\n      <ComboboxPrimitive.Input\n        data-slot=\"multi-select-input\"\n        className={cn(\n          \"h-6 min-w-16 flex-1 bg-transparent px-1 outline-none\",\n          \"placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground\",\n          className\n        )}\n        {...props}\n      />\n    </ComboboxPrimitive.Chips>\n  );\n}\n\n// Popup-side parts are the Combobox parts, aliased for a MultiSelect* call site\n// (divergence 1 — they keep their `data-slot=\"combobox-…\"`).\nconst MultiSelectContent = ComboboxContent;\nconst MultiSelectList = ComboboxList;\nconst MultiSelectItem = ComboboxItem;\nconst MultiSelectGroup = ComboboxGroup;\nconst MultiSelectLabel = ComboboxLabel;\nconst MultiSelectEmpty = ComboboxEmpty;\nconst MultiSelectSeparator = ComboboxSeparator;\n\nexport {\n  MultiSelect,\n  MultiSelectInput,\n  MultiSelectChip,\n  MultiSelectContent,\n  MultiSelectList,\n  MultiSelectItem,\n  MultiSelectGroup,\n  MultiSelectLabel,\n  MultiSelectEmpty,\n  MultiSelectSeparator,\n};\n"
    }
  ],
  "docs": "Pass `items` to `<MultiSelect>` (multiple selection is on by default) and render the list with a render-function child, exactly like Combobox. Selected values appear as chips inside `MultiSelectInput`:\n\n```tsx\n<MultiSelect items={fruits}>\n  <MultiSelectInput placeholder=\"Pick fruits\" />\n  <MultiSelectContent>\n    <MultiSelectEmpty>No fruits found.</MultiSelectEmpty>\n    <MultiSelectList>\n      {(item) => (\n        <MultiSelectItem key={item} value={item}>\n          {item}\n        </MultiSelectItem>\n      )}\n    </MultiSelectList>\n  </MultiSelectContent>\n</MultiSelect>\n```\n\nWith no children, each selected value renders as a `MultiSelectChip` showing `String(value)`. For `{ value, label }` items or custom chip styling, pass a render function to `MultiSelectInput` — it receives each selected value: `<MultiSelectInput>{(v) => <MultiSelectChip key={v.value}>{v.label}</MultiSelectChip>}</MultiSelectInput>`. The chips field bakes in NO clear-all or chevron (Backspace clears the last chip; each chip is removable); compose `@base-ui/react/combobox`'s `Clear`/`Trigger` directly if you want them. The popup-side parts are re-exports of the `combobox` item's parts (installed automatically via `registryDependencies`), so they keep their `data-slot=\"combobox-…\"` values. See `multi-select.tsx`'s doc comment for the full reuse model and the Base UI chips keyboard contract."
}
