{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"combobox","title":"Combobox","description":"Searchable single- and multi-selection control with animated chips, grouped results and form integration.","dependencies":["@base-ui/react@1.7.0","lucide-react@1.18.0"],"registryDependencies":["@siteplane/base","@siteplane/button","@siteplane/input","@siteplane/scroll-area","@siteplane/motion"],"files":[{"path":"packages/ui/src/components/combobox/combobox.tsx","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, type WithStringClassName } from \"../../system/utils/cn\";\nimport { slotClass, withClass } from \"../../system/utils/slot-class\";\nimport { useReducedMotionPreference } from \"../../system/motion/use-reduced-motion-preference\";\nimport {\n  type SiteplaneControlSize,\n  SiteplaneUIPortalScope,\n  useResolvedControlSize,\n} from \"../../system/provider/siteplane-ui-provider\";\nimport {\n  ChoiceMenuItemCheck,\n  choiceMenuItemEndGridClassName,\n  choiceMenuItemEndLabelClassName,\n  menuRowLabel,\n} from \"../../system/styles/choice-menu-item\";\nimport {\n  formControlSurfaceClassNames,\n  type FormControlSurface,\n  surfaceClassNames,\n} from \"../../system/styles/surface\";\nimport { KitInput } from \"../input/input-internal\";\nimport { ExternalInputOwner } from \"../input/input\";\nimport { Button } from \"../button\";\nimport { KitButton } from \"../button/button-internal\";\nimport { GlideOnChange, motionGhostAttribute } from \"../motion/motion-presence\";\nimport { ScrollArea } from \"../scroll-area\";\n\nexport const ComboboxContext: React.Context<{\n  chipsRef: React.RefObject<Element | null> | null;\n  inputRef: React.RefObject<HTMLInputElement | null> | null;\n  multiple: boolean;\n}> = React.createContext<{\n  chipsRef: React.RefObject<Element | null> | null;\n  inputRef: React.RefObject<HTMLInputElement | null> | null;\n  multiple: boolean;\n}>({\n  chipsRef: null,\n  inputRef: null,\n  multiple: false,\n});\n\n/**\n * The control size of the Combobox's input, ComboboxChipsInput's or\n * ComboboxInput's. A popup anchored to a chips field sizes its items by it,\n * because the field grows a row for each line of chips (FB-9). Internal, beside\n * the public ComboboxContext.\n */\nconst ComboboxInputSizeContext = React.createContext<{\n  size: SiteplaneControlSize | undefined;\n  setSize: React.Dispatch<\n    React.SetStateAction<SiteplaneControlSize | undefined>\n  >;\n} | null>(null);\n\n/** Gives the Combobox the resolved control size of its input. */\nfunction useComboboxInputSize(size: SiteplaneControlSize) {\n  const setSize = React.useContext(ComboboxInputSizeContext)?.setSize;\n  React.useLayoutEffect(() => {\n    if (!setSize) return;\n    setSize(size);\n    return () => setSize((current) => (current === size ? undefined : current));\n  }, [setSize, size]);\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  const inputRef = React.useRef<HTMLInputElement | null>(null);\n  const [inputSize, setInputSize] = React.useState<SiteplaneControlSize>();\n  const inputSizeValue = React.useMemo(\n    () => ({ size: inputSize, setSize: setInputSize }),\n    [inputSize],\n  );\n  const [resetKey, setResetKey] = React.useState(0);\n\n  React.useEffect(() => {\n    if (props.value !== undefined) {\n      return;\n    }\n\n    const handleReset = (event: Event) => {\n      const form = event.target;\n      const input = inputRef.current;\n      if (\n        form instanceof HTMLFormElement &&\n        (input?.form === form ||\n          (props.form\n            ? form.id === props.form\n            : Boolean(input && form.contains(input))))\n      ) {\n        setResetKey((current) => current + 1);\n      }\n    };\n\n    document.addEventListener(\"reset\", handleReset, true);\n    return () => document.removeEventListener(\"reset\", handleReset, true);\n  }, [props.form, props.value]);\n\n  return (\n    <ComboboxContext.Provider\n      value={{ chipsRef, inputRef, multiple: !!props.multiple }}\n    >\n      <ComboboxInputSizeContext.Provider value={inputSizeValue}>\n        <ComboboxPrimitive.Root {...props} key={resetKey} />\n      </ComboboxInputSizeContext.Provider>\n    </ComboboxContext.Provider>\n  );\n}\n\nexport function ComboboxChipsInput({\n  className,\n  ref,\n  size,\n  ...props\n}: Omit<WithStringClassName<ComboboxPrimitive.Input.Props>, \"size\"> & {\n  size?: SiteplaneControlSize | 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  useComboboxInputSize(resolvedSize);\n  const contextInputRef = React.useContext(ComboboxContext).inputRef;\n  const fallbackInputRef = React.useRef<HTMLInputElement | null>(null);\n  const inputRef = contextInputRef ?? fallbackInputRef;\n  React.useImperativeHandle(ref, () => inputRef.current as HTMLInputElement);\n\n  return (\n    <ComboboxPrimitive.Input\n      className={cn(\n        slotClass(\"combobox-chips-input\", props),\n        \"min-w-12 flex-1 outline-none [[data-slot=combobox-chip]+&]:ps-0.5\",\n        // An xl field writes in 16 px, as an xl Input does.\n        resolvedSize === \"xl\" ? \"text-base\" : \"text-base sm:text-sm\",\n        resolvedSize === \"sm\" ? \"ps-1.5\" : \"ps-2\",\n        className,\n      )}\n      data-size={resolvedSize}\n      data-slot=\"combobox-chips-input\"\n      ref={inputRef}\n      size={nativeSize}\n      {...props}\n    />\n  );\n}\n\nexport type ComboboxInputProfile = \"default\" | \"embedded\";\n\nexport type ComboboxInputProps = Omit<\n  WithStringClassName<ComboboxPrimitive.Input.Props>,\n  \"render\" | \"size\"\n> & {\n  showTrigger?: boolean;\n  showClear?: boolean;\n  startAddon?: React.ReactNode;\n  size?: SiteplaneControlSize | number | undefined;\n  variant?: FormControlSurface | undefined;\n  profile?: ComboboxInputProfile;\n  ref?: React.Ref<HTMLInputElement>;\n  triggerProps?: WithStringClassName<ComboboxPrimitive.Trigger.Props>;\n  clearProps?: WithStringClassName<ComboboxPrimitive.Clear.Props>;\n};\n\nexport function ComboboxInput({\n  className,\n  disabled,\n  profile = \"default\",\n  ref,\n  showTrigger = true,\n  showClear = false,\n  startAddon,\n  size,\n  variant = \"default\",\n  triggerProps,\n  clearProps,\n  ...props\n}: ComboboxInputProps): React.ReactElement {\n  const visualSize = typeof size === \"number\" ? undefined : size;\n  const nativeSize = typeof size === \"number\" ? size : undefined;\n  const resolvedSize = useResolvedControlSize(visualSize);\n  useComboboxInputSize(resolvedSize);\n  const contextInputRef = React.useContext(ComboboxContext).inputRef;\n  const fallbackInputRef = React.useRef<HTMLInputElement | null>(null);\n  const inputRef = contextInputRef ?? fallbackInputRef;\n  React.useImperativeHandle(ref, () => inputRef.current as HTMLInputElement);\n\n  return (\n    <ComboboxPrimitive.InputGroup\n      className=\"sp-combobox-input-group relative flex h-fit not-has-[>*.w-full]:w-fit w-full text-foreground\"\n      data-slot=\"combobox-input-group\"\n    >\n      {startAddon && (\n        <div\n          aria-hidden=\"true\"\n          className=\"sp-combobox-start-addon pointer-events-none absolute inset-y-0 start-px z-10 flex items-center [&_svg]:-mx-0.5\"\n          data-slot=\"combobox-start-addon\"\n        >\n          {startAddon}\n        </div>\n      )}\n      {/* Combobox.Input already connects the input to Field. */}\n      <ExternalInputOwner>\n        <ComboboxPrimitive.Input\n          {...props}\n          className={cn(\"sp-combobox-input\", className)}\n          data-inset-end={showTrigger || showClear ? \"\" : undefined}\n          data-inset-start={startAddon ? \"\" : undefined}\n          data-profile=\"combobox\"\n          data-profile-mode={profile}\n          data-size={resolvedSize}\n          data-slot=\"combobox-input\"\n          render={\n            <KitInput\n              insetEnd={showTrigger || showClear}\n              insetStart={Boolean(startAddon)}\n              nativeInput\n              profile=\"combobox\"\n              profileMode={profile}\n              size={nativeSize ?? resolvedSize}\n              unstyled={profile === \"embedded\"}\n              variant={variant}\n            />\n          }\n          disabled={disabled}\n          ref={inputRef}\n        />\n      </ExternalInputOwner>\n      {showTrigger && (\n        <ComboboxTrigger\n          {...triggerProps}\n          aria-label={triggerProps?.[\"aria-label\"] ?? \"Show options\"}\n          className={cn(\n            \"absolute top-1/2 -translate-y-1/2 has-[+[data-slot=combobox-clear]]:hidden\",\n            triggerProps?.className,\n          )}\n          disabled={disabled || triggerProps?.disabled}\n          render={\n            triggerProps?.render ?? (\n              <Button\n                profile=\"field-action\"\n                size={resolvedSize === \"xl\" ? \"icon-sm\" : \"icon-xs\"}\n                variant=\"ghost\"\n              />\n            )\n          }\n        >\n          <ComboboxPrimitive.Icon\n            className=\"sp-combobox-icon\"\n            data-slot=\"combobox-icon\"\n          >\n            <ChevronsUpDownIcon className=\"size-4\" />\n          </ComboboxPrimitive.Icon>\n        </ComboboxTrigger>\n      )}\n      {showClear && (\n        <ComboboxClear\n          {...clearProps}\n          aria-label={clearProps?.[\"aria-label\"] ?? \"Clear selection\"}\n          className={cn(\n            \"absolute top-1/2 -translate-y-1/2 has-[+[data-slot=combobox-clear]]:hidden\",\n            clearProps?.className,\n          )}\n          disabled={disabled || clearProps?.disabled}\n          render={\n            clearProps?.render ?? (\n              <Button\n                profile=\"field-action\"\n                size={resolvedSize === \"xl\" ? \"icon-sm\" : \"icon-xs\"}\n                variant=\"ghost\"\n              />\n            )\n          }\n        >\n          <XIcon className=\"size-3\" />\n        </ComboboxClear>\n      )}\n    </ComboboxPrimitive.InputGroup>\n  );\n}\n\nexport function ComboboxTrigger({\n  className,\n  children,\n  render,\n  ...props\n}: WithStringClassName<ComboboxPrimitive.Trigger.Props>): React.ReactElement {\n  return (\n    <ComboboxPrimitive.Trigger\n      className={cn(slotClass(\"combobox-trigger\", props, render), className)}\n      data-slot=\"combobox-trigger\"\n      render={\n        render ?? (\n          <Button profile=\"field-action\" shape=\"square\" variant=\"ghost\" />\n        )\n      }\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}: WithStringClassName<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?: WithStringClassName<ComboboxPrimitive.Portal.Props>;\n}): React.ReactElement {\n  const { chipsRef, multiple } = React.useContext(ComboboxContext);\n  const anchor = anchorProp ?? (multiple ? chipsRef : undefined);\n  // An item is as tall as the one-line field the popup anchors to (the item\n  // rule in design-rework.css). A chips field grows a row for each line of\n  // chips, so a popup anchored to it names the control size of its input\n  // instead, and its items take that size's height (FB-9).\n  const contextSize = useResolvedControlSize(undefined);\n  const inputSize = React.useContext(ComboboxInputSizeContext)?.size;\n  const itemSize =\n    anchorProp === undefined && multiple\n      ? (inputSize ?? contextSize)\n      : undefined;\n\n  return (\n    <ComboboxPrimitive.Portal {...portalProps}>\n      <SiteplaneUIPortalScope>\n        <ComboboxPrimitive.Positioner\n          align={align}\n          alignOffset={alignOffset}\n          anchor={anchor}\n          className=\"sp-combobox-positioner z-50 select-none\"\n          data-slot=\"combobox-positioner\"\n          side={side}\n          sideOffset={sideOffset}\n        >\n          <ComboboxPrimitive.Popup\n            className={cn(\n              slotClass(\"combobox-popup\", props),\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 overflow-hidden rounded-lg text-foreground before:rounded-[inherit]\",\n              className,\n            )}\n            data-item-size={itemSize}\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  indicatorPosition = \"start\",\n  onPointerCancel,\n  onPointerDown,\n  onPointerMove,\n  onPointerUp,\n  ...props\n}: WithStringClassName<ComboboxPrimitive.Item.Props> & {\n  /**\n   * Where the item shows its check: in the leading slot (`start`), or at the\n   * end of the row (`end`) for identity rows that lead with an avatar or a\n   * logo, such as the people of an assignee picker.\n   */\n  indicatorPosition?: \"start\" | \"end\" | undefined;\n}): React.ReactElement {\n  const touchPointerRef = React.useRef<{\n    id: number;\n    moved: boolean;\n    x: number;\n    y: number;\n  } | null>(null);\n  // An identity row is the end-indicator radio row of the menus (rulebook I8):\n  // it takes their grid and label, so its avatar or logo leads in the check's\n  // slot, a row of text alone keeps that slot, and the label keeps one line.\n  // The check keeps its 8 px inset, now from the end edge.\n  const end = indicatorPosition === \"end\";\n  const indicator = (\n    <ComboboxPrimitive.ItemIndicator\n      className={cn(\n        \"sp-combobox-item-indicator\",\n        end ? \"col-start-2\" : \"col-start-1\",\n        \"flex min-h-lh items-center\",\n      )}\n      data-slot=\"combobox-item-indicator\"\n      keepMounted\n    >\n      <ChoiceMenuItemCheck />\n    </ComboboxPrimitive.ItemIndicator>\n  );\n  const label = end ? (\n    <div\n      className={cn(\"sp-combobox-item-label\", choiceMenuItemEndLabelClassName)}\n      data-slot=\"combobox-item-label\"\n    >\n      {menuRowLabel(children)}\n    </div>\n  ) : (\n    <div\n      className=\"sp-combobox-item-label col-start-2 flex min-w-0 items-center gap-2\"\n      data-slot=\"combobox-item-label\"\n    >\n      {children}\n    </div>\n  );\n\n  return (\n    <ComboboxPrimitive.Item\n      className={cn(\n        slotClass(\"combobox-item\", props),\n        \"grid in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] cursor-pointer items-center gap-2 rounded-sm py-1 ps-2 text-base outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-(--sp-rw-disabled-opacity-action) 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        end\n          ? cn(choiceMenuItemEndGridClassName, \"pe-2\")\n          : \"grid-cols-[1rem_1fr] pe-4\",\n        // Rulebook I5: the check sits on the first line of a text label that\n        // wraps. The row's one track is centred in the min-height, so a\n        // one-line label stays centred, and the check's box is one line high.\n        // A label that leads with an avatar or an icon keeps the row centre,\n        // and so does a check at the end of the row.\n        !end &&\n          \"[&:not(:has(>[data-slot=combobox-item-label]>*))]:content-center [&:not(:has(>[data-slot=combobox-item-label]>*))]:items-start\",\n        className,\n      )}\n      data-indicator-position={end ? \"end\" : undefined}\n      data-slot=\"combobox-item\"\n      onPointerCancel={(event) => {\n        touchPointerRef.current = null;\n        onPointerCancel?.(event);\n      }}\n      onPointerDown={(event) => {\n        if (event.pointerType === \"touch\") {\n          touchPointerRef.current = {\n            id: event.pointerId,\n            moved: false,\n            x: event.clientX,\n            y: event.clientY,\n          };\n        }\n        onPointerDown?.(event);\n      }}\n      onPointerMove={(event) => {\n        const pointer = touchPointerRef.current;\n        if (\n          pointer?.id === event.pointerId &&\n          (Math.abs(event.clientX - pointer.x) > 8 ||\n            Math.abs(event.clientY - pointer.y) > 8)\n        ) {\n          pointer.moved = true;\n        }\n        onPointerMove?.(event);\n      }}\n      onPointerUp={(event) => {\n        const pointer = touchPointerRef.current;\n        touchPointerRef.current = null;\n        onPointerUp?.(event);\n\n        if (pointer?.id !== event.pointerId || pointer.moved) {\n          return;\n        }\n\n        const item = event.currentTarget;\n        let nativeClickFired = false;\n        const markNativeClick = () => {\n          nativeClickFired = true;\n        };\n        item.addEventListener(\"click\", markNativeClick, { once: true });\n        window.setTimeout(() => {\n          item.removeEventListener(\"click\", markNativeClick);\n          if (!nativeClickFired && item.isConnected) {\n            item.click();\n          }\n        });\n      }}\n      {...props}\n    >\n      {end ? label : indicator}\n      {end ? indicator : label}\n    </ComboboxPrimitive.Item>\n  );\n}\n\nexport function ComboboxSeparator({\n  className,\n  ...props\n}: WithStringClassName<ComboboxPrimitive.Separator.Props>): React.ReactElement {\n  return (\n    <ComboboxPrimitive.Separator\n      className={cn(\n        slotClass(\"combobox-separator\", props),\n        \"mx-2 my-1 h-px bg-border last:hidden\",\n        className,\n      )}\n      data-slot=\"combobox-separator\"\n      {...props}\n    />\n  );\n}\n\nexport function ComboboxGroup({\n  className,\n  ...props\n}: WithStringClassName<ComboboxPrimitive.Group.Props>): React.ReactElement {\n  return (\n    <ComboboxPrimitive.Group\n      className={cn(\n        slotClass(\"combobox-group\", props),\n        \"[[role=group]+&]:mt-1.5\",\n        className,\n      )}\n      data-slot=\"combobox-group\"\n      {...props}\n    />\n  );\n}\n\nexport function ComboboxGroupLabel({\n  className,\n  ...props\n}: WithStringClassName<ComboboxPrimitive.GroupLabel.Props>): React.ReactElement {\n  return (\n    <ComboboxPrimitive.GroupLabel\n      className={cn(\n        slotClass(\"combobox-group-label\", props),\n        \"px-2 py-1.5 font-(--sp-rw-font-weight-label) 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}: WithStringClassName<ComboboxPrimitive.Empty.Props>): React.ReactElement {\n  return (\n    <ComboboxPrimitive.Empty\n      className={cn(\n        slotClass(\"combobox-empty\", props),\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}: WithStringClassName<ComboboxPrimitive.Row.Props>): React.ReactElement {\n  return (\n    <ComboboxPrimitive.Row\n      className={cn(slotClass(\"combobox-row\", props), className)}\n      data-slot=\"combobox-row\"\n      {...props}\n    />\n  );\n}\n\n/** The chips, the input and the start addon of a chips field. */\nfunction chipsParts(chips: Element | null | undefined): HTMLElement[] {\n  return chips\n    ? [...chips.children].filter(\n        (child): child is HTMLElement =>\n          child instanceof HTMLElement &&\n          !child.hasAttribute(motionGhostAttribute),\n      )\n    : [];\n}\n\nexport function ComboboxValue({\n  children,\n  ...props\n}: ComboboxPrimitive.Value.Props): React.ReactElement {\n  const { chipsRef, multiple } = React.useContext(ComboboxContext);\n  const reduced = useReducedMotionPreference();\n  // The chips of a multiple selection come and go with the value (rulebook M6,\n  // M7): a chip that is added fades in, a removed one fades out where it was,\n  // and the chips and the input after them glide to their new place. Base UI\n  // drops a removed chip at once, so the keyboard order of the chips stays its\n  // own; what fades is a picture of it.\n  const glide =\n    multiple && typeof children === \"function\"\n      ? (value: unknown) => (\n          <>\n            {children(value)}\n            <GlideOnChange\n              change={value}\n              origin={() => chipsRef?.current as HTMLElement | null}\n              parts={() => chipsParts(chipsRef?.current)}\n              presence\n              reduced={reduced}\n            />\n          </>\n        )\n      : children;\n  return (\n    <ComboboxPrimitive.Value data-slot=\"combobox-value\" {...props}>\n      {glide}\n    </ComboboxPrimitive.Value>\n  );\n}\n\nexport function ComboboxList({\n  className,\n  ...props\n}: WithStringClassName<ComboboxPrimitive.List.Props>): React.ReactElement {\n  return (\n    <ScrollArea\n      className=\"grid h-auto min-h-0 flex-1 overflow-hidden [&>[data-slot=scroll-area-viewport]]:h-auto [&>[data-slot=scroll-area-viewport]]:min-h-0\"\n      scrollbarGutter\n      scrollFade\n    >\n      <ComboboxPrimitive.List\n        className={cn(\n          slotClass(\"combobox-list\", props),\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  render,\n  ...props\n}: WithStringClassName<ComboboxPrimitive.Clear.Props>): React.ReactElement {\n  return (\n    <ComboboxPrimitive.Clear\n      className={cn(slotClass(\"combobox-clear\", props, render), className)}\n      data-slot=\"combobox-clear\"\n      render={\n        render ?? (\n          <Button profile=\"field-action\" shape=\"square\" variant=\"ghost\" />\n        )\n      }\n      {...props}\n    />\n  );\n}\n\nexport function ComboboxStatus({\n  className,\n  ...props\n}: WithStringClassName<ComboboxPrimitive.Status.Props>): React.ReactElement {\n  return (\n    <ComboboxPrimitive.Status\n      className={cn(\n        slotClass(\"combobox-status\", props),\n        \"px-3 py-2 font-(--sp-rw-font-weight-label) 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  variant = \"default\",\n  layout = \"input\",\n  ...props\n}: WithStringClassName<ComboboxPrimitive.Chips.Props> & {\n  startAddon?: React.ReactNode;\n  variant?: FormControlSurface | undefined;\n  layout?: \"input\" | \"standalone\" | undefined;\n}): React.ReactElement {\n  const { chipsRef } = React.useContext(ComboboxContext);\n\n  return (\n    <ComboboxPrimitive.Chips\n      className={cn(\n        slotClass(\"combobox-chips\", props),\n        layout === \"input\" && formControlSurfaceClassNames[variant],\n        \"relative inline-flex w-full flex-wrap outline-none has-disabled:pointer-events-none\",\n        className,\n      )}\n      data-slot=\"combobox-chips\"\n      data-layout={layout}\n      data-variant={variant}\n      ref={chipsRef as React.Ref<HTMLDivElement> | null}\n      {...props}\n    >\n      {startAddon && (\n        <div\n          aria-hidden=\"true\"\n          className=\"sp-combobox-start-addon 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\n/** The plain text of a chip's value, for the title of its label. */\nfunction chipText(node: React.ReactNode): string {\n  if (typeof node === \"string\" || typeof node === \"number\") return `${node}`;\n  if (Array.isArray(node)) return node.map(chipText).join(\"\").trim();\n  if (React.isValidElement<{ children?: React.ReactNode }>(node))\n    return chipText(node.props.children);\n  return \"\";\n}\n\nexport function ComboboxChip({\n  children,\n  removeProps,\n  render,\n  startAddon,\n  ...props\n}: WithStringClassName<ComboboxPrimitive.Chip.Props> & {\n  removeProps?: WithStringClassName<ComboboxPrimitive.ChipRemove.Props>;\n  /**\n   * A small decorative avatar or icon before the value, such as the portrait of\n   * a person. The chip starts closer to its edge when it has one.\n   */\n  startAddon?: React.ReactNode;\n}): React.ReactElement {\n  // The remove action is named \"Remove\" and the chip's value, such as\n  // \"Remove Mara Ellis\", so each one in a row of chips is distinct.\n  const labelId = React.useId();\n  const removeId = React.useId();\n\n  // A chip comes, goes and glides with the value (ComboboxValue).\n  return (\n    <ComboboxPrimitive.Chip\n      data-slot=\"combobox-chip\"\n      render={render}\n      {...props}\n      // A className of the app's replaces the chip's own classes; the slot's\n      // class stays either way.\n      className={withClass(\n        props.className ?? \"flex min-w-0 max-w-full items-center outline-none\",\n        slotClass(\"combobox-chip\", props, render),\n      )}\n    >\n      {startAddon && (\n        <span\n          aria-hidden=\"true\"\n          className=\"sp-combobox-chip-start-addon me-1 flex shrink-0 items-center [&_svg:not([class*='size-'])]:size-3.5\"\n          data-slot=\"combobox-chip-start-addon\"\n        >\n          {startAddon}\n        </span>\n      )}\n      {/* A long value ends in an ellipsis on the chip's one line, at the\n          width the chips give it, and the title carries the full value\n          (rulebook K5). The badge label rule owns it (design-rework.css). */}\n      <span\n        className=\"sp-combobox-chip-label\"\n        data-slot=\"combobox-chip-label\"\n        id={labelId}\n        title={chipText(children) || undefined}\n      >\n        {children}\n      </span>\n      <ComboboxChipRemove\n        aria-labelledby={`${removeId} ${labelId}`}\n        id={removeId}\n        {...removeProps}\n      />\n    </ComboboxPrimitive.Chip>\n  );\n}\n\nexport function ComboboxChipRemove({\n  render,\n  ...props\n}: WithStringClassName<ComboboxPrimitive.ChipRemove.Props>): React.ReactElement {\n  return (\n    <ComboboxPrimitive.ChipRemove\n      aria-label=\"Remove\"\n      data-slot=\"combobox-chip-remove\"\n      render={\n        render ?? (\n          <KitButton profile=\"chip-remove\" size=\"icon-2xs\" variant=\"text\" />\n        )\n      }\n      {...props}\n      // A className of the app's replaces the action's own classes; the\n      // slot's class stays either way. The chassis keeps the kit's action at\n      // its width; an element of the app's render takes shrink-0 for that.\n      className={withClass(\n        props.className ?? (render ? \"me-0.5 shrink-0\" : \"me-0.5\"),\n        slotClass(\"combobox-chip-remove\", props, render),\n      )}\n    >\n      <XIcon className=\"size-3\" />\n    </ComboboxPrimitive.ChipRemove>\n  );\n}\n\nexport const useComboboxFilter: typeof ComboboxPrimitive.useFilter =\n  ComboboxPrimitive.useFilter;\n\nexport { ComboboxPrimitive };\n","type":"registry:ui","target":"~/src/siteplane/components/combobox/combobox.tsx"},{"path":"packages/ui/src/components/combobox/index.ts","content":"export * from \"./combobox\";\n","type":"registry:ui","target":"~/src/siteplane/components/combobox/index.ts"}],"type":"registry:ui","docs":"https://ui.siteplane.io/docs-markdown/components/combobox.md","categories":["Forms & selection"],"meta":{"avoidWhen":["Avoid when the options are few and need no search; use Select.","Avoid when the text may differ from the suggestions; use Autocomplete."],"description":"Searchable single- and multi-selection control with animated chips, grouped results and form integration.","knownLimitations":["For async options the application owns fetching, stale results and the pending and error states."],"providerNotes":[],"releaseStatus":"stable","searchTerms":["combobox"],"useWhen":["Use to choose one or several options from a known list with search, such as assigning people or tags."]}}