{
  "name": "combobox",
  "type": "registry:ui",
  "title": "Combobox",
  "description": "Combobox provides searchable single- and multi-select lists, including chip-based selection.",
  "registryDependencies": [
    "@siteplane/base",
    "@siteplane/input",
    "@siteplane/scroll-area"
  ],
  "dependencies": [
    "@base-ui/react@1.5.0",
    "lucide-react@1.18.0"
  ],
  "files": [
    {
      "path": "src/components/ui/combobox.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { Combobox as ComboboxPrimitive } from \"@base-ui/react/combobox\";\nimport { ChevronsUpDownIcon, XIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  SiteplaneUIPortalScope,\n  useResolvedControlSize,\n} from \"@/components/ui/siteplane-provider\";\nimport { CheckIcon } from \"@/components/ui/icons\";\nimport { Input } from \"@/components/ui/input\";\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\nimport { surfaceClassNames } from \"@/components/ui/surface\";\n\nexport const ComboboxContext: React.Context<{\n  chipsRef: React.RefObject<Element | null> | null;\n  multiple: boolean;\n}> = React.createContext<{\n  chipsRef: React.RefObject<Element | null> | null;\n  multiple: boolean;\n}>({\n  chipsRef: null,\n  multiple: false,\n});\n\nexport function Combobox<Value, Multiple extends boolean | undefined = false>(\n  props: ComboboxPrimitive.Root.Props<Value, Multiple>,\n): React.ReactElement {\n  const chipsRef = React.useRef<Element | null>(null);\n  return (\n    <ComboboxContext.Provider value={{ chipsRef, multiple: !!props.multiple }}>\n      <ComboboxPrimitive.Root {...props} />\n    </ComboboxContext.Provider>\n  );\n}\n\nexport function ComboboxChipsInput({\n  className,\n  size,\n  ...props\n}: Omit<ComboboxPrimitive.Input.Props, \"size\"> & {\n  size?: \"sm\" | \"default\" | \"lg\" | \"xl\" | number;\n  ref?: React.Ref<HTMLInputElement>;\n}): React.ReactElement {\n  const visualSize = typeof size === \"number\" ? undefined : size;\n  const nativeSize = typeof size === \"number\" ? size : undefined;\n  const resolvedSize = useResolvedControlSize(visualSize);\n\n  return (\n    <ComboboxPrimitive.Input\n      className={cn(\n        \"min-w-12 flex-1 text-base outline-none sm:text-sm [[data-slot=combobox-chip]+&]:ps-0.5\",\n        resolvedSize === \"sm\" ? \"ps-1.5\" : \"ps-2\",\n        className,\n      )}\n      data-size={resolvedSize}\n      data-slot=\"combobox-chips-input\"\n      size={nativeSize}\n      {...props}\n    />\n  );\n}\n\nexport function ComboboxInput({\n  className,\n  showTrigger = true,\n  showClear = false,\n  startAddon,\n  size,\n  triggerProps,\n  clearProps,\n  ...props\n}: Omit<ComboboxPrimitive.Input.Props, \"size\"> & {\n  showTrigger?: boolean;\n  showClear?: boolean;\n  startAddon?: React.ReactNode;\n  size?: \"sm\" | \"default\" | \"lg\" | \"xl\" | number;\n  ref?: React.Ref<HTMLInputElement>;\n  triggerProps?: ComboboxPrimitive.Trigger.Props;\n  clearProps?: ComboboxPrimitive.Clear.Props;\n}): React.ReactElement {\n  const visualSize = typeof size === \"number\" ? undefined : size;\n  const nativeSize = typeof size === \"number\" ? size : undefined;\n  const resolvedSize = useResolvedControlSize(visualSize);\n\n  return (\n    <ComboboxPrimitive.InputGroup\n      className=\"relative not-has-[>*.w-full]:w-fit w-full text-foreground has-disabled:opacity-64\"\n      data-slot=\"combobox-input-group\"\n    >\n      {startAddon && (\n        <div\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute inset-y-0 start-px z-10 flex items-center ps-[calc(--spacing(3)-1px)] opacity-80 has-[+[data-size=sm]]:ps-[calc(--spacing(2.5)-1px)] has-[+[data-size=xl]]:ps-[calc(--spacing(3.5)-1px)] [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:-mx-0.5\"\n          data-slot=\"combobox-start-addon\"\n        >\n          {startAddon}\n        </div>\n      )}\n      <ComboboxPrimitive.Input\n        className={cn(\n          startAddon &&\n            \"data-[size=sm]:*:data-[slot=input]:ps-[calc(--spacing(7.5)-1px)] data-[size=lg]:*:data-[slot=input]:ps-[calc(--spacing(9.5)-1px)] data-[size=xl]:*:data-[slot=input]:ps-[calc(--spacing(10.5)-1px)] *:data-[slot=input]:ps-[calc(--spacing(8.5)-1px)] sm:data-[size=sm]:*:data-[slot=input]:ps-[calc(--spacing(7)-1px)] sm:data-[size=lg]:*:data-[slot=input]:ps-[calc(--spacing(9)-1px)] sm:data-[size=xl]:*:data-[slot=input]:ps-[calc(--spacing(10)-1px)] sm:*:data-[slot=input]:ps-[calc(--spacing(8)-1px)]\",\n          resolvedSize === \"sm\"\n            ? \"has-[+[data-slot=combobox-trigger],+[data-slot=combobox-clear]]:*:data-[slot=input]:pe-6.5\"\n            : resolvedSize === \"xl\"\n              ? \"has-[+[data-slot=combobox-trigger],+[data-slot=combobox-clear]]:*:data-[slot=input]:pe-8\"\n            : \"has-[+[data-slot=combobox-trigger],+[data-slot=combobox-clear]]:*:data-[slot=input]:pe-7\",\n          className,\n        )}\n        data-size={resolvedSize}\n        data-slot=\"combobox-input\"\n        render={\n          <Input\n            className=\"has-disabled:opacity-100\"\n            nativeInput\n            size={nativeSize ?? resolvedSize}\n          />\n        }\n        {...props}\n      />\n      {showTrigger && (\n        <ComboboxTrigger\n          aria-label=\"Show options\"\n          className={cn(\n            \"absolute top-1/2 inline-flex size-8 shrink-0 -translate-y-1/2 cursor-pointer items-center justify-center rounded-md border border-transparent opacity-80 outline-none transition-opacity pointer-coarse:after:absolute pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:opacity-100 has-[+[data-slot=combobox-clear]]:hidden sm:size-7 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0\",\n            resolvedSize === \"sm\" ? \"end-0\" : \"end-0.5\",\n            resolvedSize === \"xl\" && \"size-9 sm:size-8\",\n          )}\n          {...triggerProps}\n        >\n          <ComboboxPrimitive.Icon data-slot=\"combobox-icon\">\n            <ChevronsUpDownIcon />\n          </ComboboxPrimitive.Icon>\n        </ComboboxTrigger>\n      )}\n      {showClear && (\n        <ComboboxClear\n          aria-label=\"Clear selection\"\n          className={cn(\n            \"absolute top-1/2 inline-flex size-8 shrink-0 -translate-y-1/2 cursor-pointer items-center justify-center rounded-md border border-transparent opacity-80 outline-none transition-opacity pointer-coarse:after:absolute pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:opacity-100 has-[+[data-slot=combobox-clear]]:hidden sm:size-7 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0\",\n            resolvedSize === \"sm\" ? \"end-0\" : \"end-0.5\",\n            resolvedSize === \"xl\" && \"size-9 sm:size-8\",\n          )}\n          {...clearProps}\n        >\n          <XIcon />\n        </ComboboxClear>\n      )}\n    </ComboboxPrimitive.InputGroup>\n  );\n}\n\nexport function ComboboxTrigger({\n  className,\n  children,\n  ...props\n}: ComboboxPrimitive.Trigger.Props): React.ReactElement {\n  return (\n    <ComboboxPrimitive.Trigger\n      className={className}\n      data-slot=\"combobox-trigger\"\n      {...props}\n    >\n      {children}\n    </ComboboxPrimitive.Trigger>\n  );\n}\n\nexport function ComboboxPopup({\n  className,\n  children,\n  side = \"bottom\",\n  sideOffset = 4,\n  alignOffset,\n  align = \"start\",\n  anchor: anchorProp,\n  portalProps,\n  ...props\n}: ComboboxPrimitive.Popup.Props & {\n  align?: ComboboxPrimitive.Positioner.Props[\"align\"];\n  sideOffset?: ComboboxPrimitive.Positioner.Props[\"sideOffset\"];\n  alignOffset?: ComboboxPrimitive.Positioner.Props[\"alignOffset\"];\n  side?: ComboboxPrimitive.Positioner.Props[\"side\"];\n  anchor?: ComboboxPrimitive.Positioner.Props[\"anchor\"];\n  portalProps?: ComboboxPrimitive.Portal.Props;\n}): React.ReactElement {\n  const { chipsRef } = React.useContext(ComboboxContext);\n  const anchor = anchorProp ?? chipsRef;\n\n  return (\n    <ComboboxPrimitive.Portal {...portalProps}>\n      <SiteplaneUIPortalScope>\n        <ComboboxPrimitive.Positioner\n          align={align}\n          alignOffset={alignOffset}\n          anchor={anchor}\n          className=\"z-50 select-none\"\n          data-slot=\"combobox-positioner\"\n          side={side}\n          sideOffset={sideOffset}\n        >\n          <ComboboxPrimitive.Popup\n            className={cn(\n              surfaceClassNames.floatingOverlay,\n              \"flex max-h-[min(var(--available-height),23rem)] min-w-(--anchor-width) max-w-(--available-width) origin-(--transform-origin) flex-col rounded-lg text-foreground before:rounded-[inherit]\",\n              className,\n            )}\n            data-slot=\"combobox-popup\"\n            {...props}\n          >\n            {children}\n          </ComboboxPrimitive.Popup>\n        </ComboboxPrimitive.Positioner>\n      </SiteplaneUIPortalScope>\n    </ComboboxPrimitive.Portal>\n  );\n}\n\nexport function ComboboxItem({\n  className,\n  children,\n  ...props\n}: ComboboxPrimitive.Item.Props): React.ReactElement {\n  return (\n    <ComboboxPrimitive.Item\n      className={cn(\n        \"grid min-h-8 in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] cursor-pointer grid-cols-[1rem_1fr] items-center gap-2 rounded-sm py-1 ps-2 pe-4 text-base outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0\",\n        className,\n      )}\n      data-slot=\"combobox-item\"\n      {...props}\n    >\n      <ComboboxPrimitive.ItemIndicator className=\"col-start-1\">\n        <CheckIcon />\n      </ComboboxPrimitive.ItemIndicator>\n      <div className=\"col-start-2\">{children}</div>\n    </ComboboxPrimitive.Item>\n  );\n}\n\nexport function ComboboxSeparator({\n  className,\n  ...props\n}: ComboboxPrimitive.Separator.Props): React.ReactElement {\n  return (\n    <ComboboxPrimitive.Separator\n      className={cn(\"mx-2 my-1 h-px bg-border last:hidden\", className)}\n      data-slot=\"combobox-separator\"\n      {...props}\n    />\n  );\n}\n\nexport function ComboboxGroup({\n  className,\n  ...props\n}: ComboboxPrimitive.Group.Props): React.ReactElement {\n  return (\n    <ComboboxPrimitive.Group\n      className={cn(\"[[role=group]+&]:mt-1.5\", className)}\n      data-slot=\"combobox-group\"\n      {...props}\n    />\n  );\n}\n\nexport function ComboboxGroupLabel({\n  className,\n  ...props\n}: ComboboxPrimitive.GroupLabel.Props): React.ReactElement {\n  return (\n    <ComboboxPrimitive.GroupLabel\n      className={cn(\n        \"px-2 py-1.5 font-medium text-muted-foreground text-xs\",\n        className,\n      )}\n      data-slot=\"combobox-group-label\"\n      {...props}\n    />\n  );\n}\n\nexport function ComboboxEmpty({\n  className,\n  ...props\n}: ComboboxPrimitive.Empty.Props): React.ReactElement {\n  return (\n    <ComboboxPrimitive.Empty\n      className={cn(\n        \"not-empty:p-2 text-center text-base text-muted-foreground sm:text-sm\",\n        className,\n      )}\n      data-slot=\"combobox-empty\"\n      {...props}\n    />\n  );\n}\n\nexport function ComboboxRow({\n  className,\n  ...props\n}: ComboboxPrimitive.Row.Props): React.ReactElement {\n  return (\n    <ComboboxPrimitive.Row\n      className={className}\n      data-slot=\"combobox-row\"\n      {...props}\n    />\n  );\n}\n\nexport function ComboboxValue({\n  ...props\n}: ComboboxPrimitive.Value.Props): React.ReactElement {\n  return <ComboboxPrimitive.Value data-slot=\"combobox-value\" {...props} />;\n}\n\nexport function ComboboxList({\n  className,\n  ...props\n}: ComboboxPrimitive.List.Props): React.ReactElement {\n  return (\n    <ScrollArea scrollbarGutter scrollFade>\n      <ComboboxPrimitive.List\n        className={cn(\n          \"not-empty:scroll-py-1 not-empty:px-1 not-empty:py-1 in-data-has-overflow-y:pe-3\",\n          className,\n        )}\n        data-slot=\"combobox-list\"\n        {...props}\n      />\n    </ScrollArea>\n  );\n}\n\nexport function ComboboxClear({\n  className,\n  ...props\n}: ComboboxPrimitive.Clear.Props): React.ReactElement {\n  return (\n    <ComboboxPrimitive.Clear\n      className={className}\n      data-slot=\"combobox-clear\"\n      {...props}\n    />\n  );\n}\n\nexport function ComboboxStatus({\n  className,\n  ...props\n}: ComboboxPrimitive.Status.Props): React.ReactElement {\n  return (\n    <ComboboxPrimitive.Status\n      className={cn(\n        \"px-3 py-2 font-medium text-muted-foreground text-xs empty:m-0 empty:p-0\",\n        className,\n      )}\n      data-slot=\"combobox-status\"\n      {...props}\n    />\n  );\n}\n\nexport function ComboboxCollection(\n  props: ComboboxPrimitive.Collection.Props,\n): React.ReactElement {\n  return (\n    <ComboboxPrimitive.Collection data-slot=\"combobox-collection\" {...props} />\n  );\n}\n\nexport function ComboboxChips({\n  className,\n  children,\n  startAddon,\n  ...props\n}: ComboboxPrimitive.Chips.Props & {\n  startAddon?: React.ReactNode;\n}): React.ReactElement {\n  const { chipsRef } = React.useContext(ComboboxContext);\n\n  return (\n    <ComboboxPrimitive.Chips\n      className={cn(\n        surfaceClassNames.formControlWrap,\n        \"min-h-9 flex-wrap gap-1 p-[calc(--spacing(1)-1px)] *:min-h-7 has-autofill:bg-foreground/4 has-data-[size=lg]:min-h-10 has-data-[size=xl]:min-h-14 has-data-[size=xl]:text-base has-data-[size=sm]:min-h-8 has-data-[size=lg]:*:min-h-8 has-data-[size=xl]:*:min-h-10 has-data-[size=sm]:*:min-h-6 sm:min-h-8 sm:has-data-[size=lg]:min-h-9 sm:has-data-[size=xl]:min-h-14 sm:has-data-[size=xl]:text-base sm:has-data-[size=sm]:min-h-7 sm:*:min-h-6 sm:has-data-[size=lg]:*:min-h-7 sm:has-data-[size=xl]:*:min-h-10 sm:has-data-[size=sm]:*:min-h-5 dark:has-autofill:bg-foreground/8\",\n        className,\n      )}\n      data-slot=\"combobox-chips\"\n      ref={chipsRef as React.Ref<HTMLDivElement> | null}\n      {...props}\n    >\n      {startAddon && (\n        <div\n          aria-hidden=\"true\"\n          className=\"flex shrink-0 items-center ps-2 opacity-80 has-[~[data-size=sm]]:has-[+[data-slot=combobox-chip]]:pe-1.5 has-[~[data-size=sm]]:ps-1.5 has-[+[data-slot=combobox-chip]]:pe-2 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:-ms-0.5 [&_svg]:-me-1.5\"\n          data-slot=\"combobox-start-addon\"\n        >\n          {startAddon}\n        </div>\n      )}\n      {children}\n    </ComboboxPrimitive.Chips>\n  );\n}\n\nexport function ComboboxChip({\n  children,\n  removeProps,\n  ...props\n}: ComboboxPrimitive.Chip.Props & {\n  removeProps?: ComboboxPrimitive.ChipRemove.Props;\n}): React.ReactElement {\n  return (\n    <ComboboxPrimitive.Chip\n      className=\"flex items-center rounded-[calc(var(--radius-md)-1px)] bg-accent ps-2 font-medium text-accent-foreground text-sm outline-none sm:text-xs/(--text-xs--line-height) [&_svg:not([class*='size-'])]:size-4 sm:[&_svg:not([class*='size-'])]:size-3.5\"\n      data-slot=\"combobox-chip\"\n      {...props}\n    >\n      {children}\n      <ComboboxChipRemove {...removeProps} />\n    </ComboboxPrimitive.Chip>\n  );\n}\n\nexport function ComboboxChipRemove(\n  props: ComboboxPrimitive.ChipRemove.Props,\n): React.ReactElement {\n  return (\n    <ComboboxPrimitive.ChipRemove\n      aria-label=\"Remove\"\n      className=\"h-full shrink-0 cursor-pointer px-1.5 opacity-80 hover:opacity-100 [&_svg:not([class*='size-'])]:size-4 sm:[&_svg:not([class*='size-'])]:size-3.5\"\n      data-slot=\"combobox-chip-remove\"\n      {...props}\n    >\n      <XIcon />\n    </ComboboxPrimitive.ChipRemove>\n  );\n}\n\nexport const useComboboxFilter: typeof ComboboxPrimitive.useFilter =\n  ComboboxPrimitive.useFilter;\n\nexport { ComboboxPrimitive };\n"
    },
    {
      "path": "src/components/ui/icons.tsx",
      "type": "registry:ui",
      "content": "import type React from \"react\";\n\n// Siteplane-UI-Haken-Glyphe (bewusst andere Geometrie als lucide `Check`).\n// Einzige Quelle fuer den statischen Selected-Marker in Select, Combobox, Menu,\n// ContextMenu und Drawer. (Die Checkbox nutzt eine eigene, ueber\n// `siteplane-checkbox-check` animierte Variante und bleibt davon unberuehrt.)\nexport function CheckIcon(props: React.ComponentProps<\"svg\">): React.ReactElement {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      fill=\"none\"\n      height=\"24\"\n      stroke=\"currentColor\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      strokeWidth=\"2\"\n      viewBox=\"0 0 24 24\"\n      width=\"24\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      {...props}\n    >\n      <path d=\"M5.252 12.7 10.2 18.63 18.748 5.37\" />\n    </svg>\n  );\n}\n"
    }
  ],
  "categories": [
    "form-controls"
  ],
  "docs": "# Combobox\n\n> Siteplane UI release `0.1.1`.\n\n## Public Purpose\n\nCombobox provides searchable selection constrained to listed options. Use it for\nsingle select or chip-based multi-select. Use Select for a short unsearchable\nlist and Autocomplete when free text is a valid value.\n\n## Import\n\n`import { Combobox, ComboboxChip, ComboboxChipRemove, ComboboxChips, ComboboxChipsInput, ComboboxEmpty, ComboboxGroup, ComboboxGroupLabel, ComboboxInput, ComboboxItem, ComboboxList, ComboboxPopup, ComboboxRow, ComboboxStatus, ComboboxTrigger, ComboboxValue, useComboboxFilter } from \"@/components/ui/combobox\";`\n\n## Public API\n\n- Filter through root `items` or `useComboboxFilter`; do not render a\n  searchable list without filtering it.\n- `ComboboxTrigger` supports a button-triggered composition where the selected\n  value stays in the trigger and search lives in the popup.\n- Set `ComboboxInput showTrigger={false}` for a plain search field without the\n  trailing dropdown affordance.\n- `ComboboxPopup` accepts `side`, `align`, `sideOffset`, and\n  `alignOffset`; defaults are bottom and start alignment.\n- `ComboboxItem disabled` marks an unavailable option.\n- `ComboboxList` provides the bounded ScrollArea and fades for long lists;\n  `ComboboxRow` supports grid layouts.\n- `ComboboxStatus` renders loading or result status.\n- `ComboboxChips startAddon` adds leading context. Set `size` on\n  `ComboboxChipsInput` to scale the complete chips field.\n\n## Public Motion\n\n- Popup, filter, highlight, chips, and reduced-motion behavior remain native.\n- Active options and icon actions use a pointer cursor; input text keeps a text\n  cursor.\n- Results and status changes must not change the input dimensions.\n\n## Public Accessibility\n\n- Give the input or chips field a visible label, `aria-label`, or\n  `aria-labelledby`.\n- Each chip needs an accessible name; its remove control already supplies the\n  removal action semantics.\n- Keep disabled options unavailable to pointer and keyboard selection.\n- Connect invalid state to the input and render a visible `FieldError`.\n\n## Public Agent Guidance\n\n- Use Combobox for searchable constrained selection, not free-text search.\n- Keep one filtering source and render input or trigger, popup, list, and items\n  as one composition.\n- Preserve native cursor, focus, disabled, popup, scroll, and motion behavior.\n\n## Public Links\n\n- [Documentation and preview](https://ui.siteplane.io/docs/components/form-controls/combobox)\n\n- [Registry source](https://ui.siteplane.io/r/combobox.json)\n",
  "meta": {
    "siteplane:agentContractVersion": 1,
    "siteplane:docs": "https://ui.siteplane.io/docs/components/form-controls/combobox",
    "siteplane:releaseVersion": "0.1.1",
    "siteplane:sourceOwned": true
  },
  "devDependencies": []
}
