{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "combobox",
  "title": "Combobox",
  "description": "A combobox component.",
  "dependencies": [
    "@hugeicons/react",
    "@hugeicons/core-free-icons"
  ],
  "registryDependencies": [
    "@cubby-ui/label",
    "@cubby-ui/scroll-area",
    "@cubby-ui/elevated",
    "@cubby-ui/popup-search"
  ],
  "files": [
    {
      "path": "registry/default/combobox/combobox.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  elevatedSurface,\n  type SurfaceLevel,\n} from \"@/registry/default/lib/elevated\";\nimport { Label } from \"@/registry/default/label/label\";\nimport {\n  ScrollArea,\n  type ScrollAreaProps,\n} from \"@/registry/default/scroll-area/scroll-area\";\nimport { Combobox as BaseCombobox } from \"@base-ui/react/combobox\";\n\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport {\n  Cancel01Icon,\n  CheckIcon,\n  ChevronDownIcon,\n  Search01Icon,\n} from \"@hugeicons/core-free-icons\";\nimport {\n  POPUP_ITEM,\n  POPUP_SEARCH_BAND,\n  POPUP_SEARCH_CLEAR,\n  POPUP_SEARCH_FIELD,\n  POPUP_SEARCH_ICON,\n} from \"@/registry/default/lib/popup-search\";\nconst useComboboxFilter = BaseCombobox.useFilter;\nconst useComboboxFilteredItems = BaseCombobox.useFilteredItems;\nconst createComboboxItems = BaseCombobox.createItems;\n\n// Shared styling for the start/end addon containers. `pointer-events-none` lets\n// clicks on decorative content (icons, spinners) fall through to the InputGroup,\n// whose Base UI mousedown handler focuses the input; interactive children opt\n// back in so buttons/links inside an addon still receive their own clicks.\nconst comboboxAddonClassName = cn(\n  \"text-muted-foreground pointer-events-none flex shrink-0 items-center\",\n  \"[&_svg:not([class*='size-'])]:size-4\",\n  \"[&_:is(button,a,input,select,textarea,label,[role=button])]:pointer-events-auto\",\n);\n\nconst ComboboxContext = React.createContext<{\n  id: string;\n  /** The mounted ComboboxChips element, if any, used to anchor the popup. */\n  chipsElement: HTMLDivElement | null;\n  setChipsElement: (element: HTMLDivElement | null) => void;\n} | null>(null);\n\n// `Item` lets `items` accept a `createComboboxItems()` collection, whose\n// rendered item type differs from its selection value.\nfunction Combobox<\n  Value,\n  Multiple extends boolean | undefined = false,\n  Item = Value,\n>(props: BaseCombobox.Root.Props<Value, Multiple, Item>): React.JSX.Element {\n  const id = React.useId();\n  const [chipsElement, setChipsElement] = React.useState<HTMLDivElement | null>(\n    null,\n  );\n\n  const contextValue = React.useMemo(\n    () => ({ id, chipsElement, setChipsElement }),\n    [id, chipsElement],\n  );\n\n  return (\n    <ComboboxContext.Provider value={contextValue}>\n      <BaseCombobox.Root data-slot=\"combobox\" {...props} />\n    </ComboboxContext.Provider>\n  );\n}\n\nfunction ComboboxInput({\n  id: idProp,\n  className,\n  inputClassName,\n  variant = \"default\",\n  showTrigger = variant !== \"popup\",\n  showClear = true,\n  start,\n  end,\n  \"aria-label\": ariaLabelProp,\n  ...props\n}: BaseCombobox.Input.Props & {\n  showTrigger?: boolean;\n  showClear?: boolean;\n  /**\n   * `\"popup\"` is the search field at the top of a `ComboboxPopup`: drawn like\n   * the list's rows, with a search icon and no trigger by default.\n   */\n  variant?: \"default\" | \"elevated\" | \"popup\";\n  /** Content pinned to the start (leading edge) of the field, e.g. a search icon. */\n  start?: React.ReactNode;\n  /** Content pinned to the end of the field, before Clear and Trigger, e.g. a loading spinner. */\n  end?: React.ReactNode;\n  /** Class applied to the inner `<input>`. `className` styles the field wrapper. */\n  inputClassName?: string;\n}) {\n  const context = React.use(ComboboxContext);\n  const id = idProp ?? context?.id;\n  const popup = variant === \"popup\";\n  // A popup's search field has no visible label, and a placeholder isn't a\n  // name: default it, as the menus' search fields do.\n  const ariaLabel =\n    ariaLabelProp ??\n    (popup && props[\"aria-labelledby\"] === undefined ? \"Search\" : undefined);\n  const leading =\n    popup && start === undefined ? (\n      <HugeiconsIcon\n        icon={Search01Icon}\n        strokeWidth={2}\n        className={POPUP_SEARCH_ICON}\n      />\n    ) : (\n      start\n    );\n\n  const field = (\n    // The wrapper carries the field chrome (border, bg, height, focus ring) so\n    // start/end/clear/trigger lay out as flex siblings instead of overlapping\n    // the input via absolute positioning + hand-tuned padding.\n    <BaseCombobox.InputGroup\n      data-slot=\"combobox-input-group\"\n      className={cn(\n        popup\n          ? // An item's height, so the field reads as part of the list.\n            [POPUP_SEARCH_FIELD, \"cursor-text\"]\n          : [\n              \"flex h-10 w-full min-w-0 cursor-text items-center gap-2 rounded-lg border bg-clip-padding px-3 sm:not-pointer-coarse:h-9\",\n              variant === \"default\" ? \"bg-input\" : \"bg-input-elevated\",\n              // Focus ring follows the input via focus-within (same pattern as ComboboxChips).\n              \"focus-within:outline-ring/50 outline-0 outline-offset-0 outline-transparent transition-[outline-width,outline-offset,outline-color] duration-100 ease-out outline-solid focus-within:outline-2 focus-within:outline-offset-2\",\n            ],\n        // Disabled state (input or field-level) dims and locks the whole field.\n        \"has-[input:disabled]:pointer-events-none has-[input:disabled]:cursor-not-allowed has-[input:disabled]:opacity-60\",\n        className,\n      )}\n    >\n      {leading != null && (\n        <div\n          data-slot=\"combobox-input-start\"\n          className={comboboxAddonClassName}\n        >\n          {leading}\n        </div>\n      )}\n      <BaseCombobox.Input\n        id={id}\n        data-slot=\"combobox-input\"\n        className={cn(\n          \"placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground h-full min-w-0 flex-1 border-none bg-transparent p-0 text-base font-normal shadow-none outline-none disabled:cursor-not-allowed sm:not-pointer-coarse:text-sm\",\n          \"file:text-foreground file:inline-flex file:h-7 file:rounded-md file:border-0 file:bg-transparent file:text-sm file:font-medium\",\n          inputClassName,\n        )}\n        aria-label={ariaLabel}\n        {...props}\n      />\n      {end != null && (\n        <div data-slot=\"combobox-input-end\" className={comboboxAddonClassName}>\n          {end}\n        </div>\n      )}\n      {(showClear || showTrigger) && (\n        <div className=\"flex shrink-0 items-center gap-2\">\n          {showClear && (\n            <ComboboxClear className={popup ? POPUP_SEARCH_CLEAR : undefined} />\n          )}\n          {showTrigger && <ComboboxTrigger />}\n        </div>\n      )}\n    </BaseCombobox.InputGroup>\n  );\n\n  if (!popup) return field;\n\n  return (\n    // Covers the side and top gutters; the list's own top padding is the gap\n    // under the field.\n    <div data-slot=\"combobox-input-band\" className={POPUP_SEARCH_BAND}>\n      {field}\n    </div>\n  );\n}\n\nfunction ComboboxChipInput({\n  id: idProp,\n  className,\n  ...props\n}: BaseCombobox.Input.Props) {\n  const context = React.use(ComboboxContext);\n  const id = idProp ?? context?.id;\n\n  return (\n    <BaseCombobox.Input\n      id={id}\n      data-slot=\"combobox-input\"\n      className={cn(\n        \"placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground h-7 min-w-12 flex-1 rounded-none border-none bg-transparent p-0 pl-1.5 text-base font-normal shadow-none outline-none focus-visible:ring-0 focus-visible:ring-offset-0 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-60 sm:not-pointer-coarse:h-6 sm:not-pointer-coarse:text-sm\",\n\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction ComboboxTrigger({\n  className,\n  children,\n  ...props\n}: BaseCombobox.Trigger.Props) {\n  return (\n    <BaseCombobox.Trigger\n      data-slot=\"combobox-trigger\"\n      aria-label=\"Open popup\"\n      className={cn(\n        \"inline-flex size-4 cursor-pointer items-center justify-center rounded-md border-none bg-transparent p-0 text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-60\",\n        \"focus-visible:ring-ring/70 outline-none focus-visible:ring-2\",\n        className,\n      )}\n      {...props}\n    >\n      {children ?? (\n        <HugeiconsIcon\n          icon={ChevronDownIcon}\n          className=\"h-4 w-4\"\n          strokeWidth={2}\n        />\n      )}\n    </BaseCombobox.Trigger>\n  );\n}\n\nfunction ComboboxIcon({\n  className,\n  ...props\n}: React.ComponentProps<typeof BaseCombobox.Icon>) {\n  return (\n    <BaseCombobox.Icon\n      data-slot=\"combobox-icon\"\n      className={cn(\"ml-2 h-4 w-4 shrink-0 opacity-50\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction ComboboxClear({ className, ...props }: BaseCombobox.Clear.Props) {\n  return (\n    <BaseCombobox.Clear\n      data-slot=\"combobox-clear\"\n      aria-label=\"Clear selection\"\n      className={cn(\n        \"inline-flex h-4 w-4 cursor-pointer items-center justify-center rounded-sm opacity-70 transition-[opacity,scale,transform,translate] hover:opacity-100 disabled:pointer-events-none\",\n        \"focus-visible:ring-ring/70 duration-100 outline-none focus-visible:ring-2\",\n        \"data-ending-style:translate-x-1 data-ending-style:opacity-0 data-starting-style:translate-x-1 data-starting-style:opacity-0\",\n        className,\n      )}\n      {...props}\n    >\n      <HugeiconsIcon icon={Cancel01Icon} className=\"h-4 w-4\" strokeWidth={2} />\n    </BaseCombobox.Clear>\n  );\n}\n\nfunction ComboboxValue({ ...props }: BaseCombobox.Value.Props) {\n  return <BaseCombobox.Value data-slot=\"combobox-value\" {...props} />;\n}\n\nfunction ComboboxPortal({ ...props }: BaseCombobox.Portal.Props) {\n  return <BaseCombobox.Portal data-slot=\"combobox-portal\" {...props} />;\n}\n\nfunction ComboboxBackdrop({\n  className,\n  ...props\n}: BaseCombobox.Backdrop.Props) {\n  return (\n    <BaseCombobox.Backdrop\n      data-slot=\"combobox-backdrop\"\n      className={cn(\n        \"fixed inset-0 z-30 bg-black/50 data-ending-style:opacity-0 data-starting-style:opacity-0\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction ComboboxPositioner({\n  className,\n  ...props\n}: BaseCombobox.Positioner.Props) {\n  return (\n    <BaseCombobox.Positioner\n      data-slot=\"combobox-positioner\"\n      sideOffset={6}\n      // z-50 (matching Select's positioner) keeps the portaled popup above\n      // overlay surfaces it's opened from — e.g. the mobile Drawer, whose\n      // viewport is z-50. Without it the popup renders behind the drawer.\n      className={cn(\"z-50\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction ComboboxPopupPrimitive({\n  className,\n  level = 3,\n  shadowLevel = 3,\n  ...props\n}: BaseCombobox.Popup.Props & {\n  /** Surface elevation level for the popup bg (1-8). Bump when nesting inside a Dialog. Defaults to 3. */\n  level?: SurfaceLevel;\n  /** Shadow weight (1-8). Pinned to 3 by default so the combobox reads the same regardless of nesting depth. */\n  shadowLevel?: SurfaceLevel;\n}) {\n  return (\n    <BaseCombobox.Popup\n      data-slot=\"combobox-popup\"\n      data-level={level}\n      className={cn(\n        \"text-popover-foreground ease-out-expo flex max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) origin-(--transform-origin) flex-col overflow-clip overscroll-contain rounded-xl transition-[transform,scale,opacity] duration-100 data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0\",\n        // Use elevatedSurface (rim on ::after) because combobox group labels are\n        // often sticky and would otherwise hide the rim where they sit.\n        elevatedSurface(level, shadowLevel),\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction ComboboxArrow({ className, ...props }: BaseCombobox.Arrow.Props) {\n  return (\n    <BaseCombobox.Arrow\n      data-slot=\"combobox-arrow\"\n      className={cn(\n        \"data-[side=bottom]:top-[-8px] data-[side=left]:right-[-13px] data-[side=left]:rotate-90 data-[side=right]:left-[-13px] data-[side=right]:-rotate-90 data-[side=top]:bottom-[-8px] data-[side=top]:rotate-180\",\n        className,\n      )}\n      {...props}\n    >\n      <svg width=\"20\" height=\"10\" viewBox=\"0 0 20 10\" fill=\"none\">\n        <path\n          d=\"M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V9H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z\"\n          className=\"fill-(--popup-surface,var(--popover))\"\n        />\n        <path\n          d=\"M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z\"\n          className=\"fill-border/70\"\n        />\n      </svg>\n    </BaseCombobox.Arrow>\n  );\n}\n\nfunction ComboboxStatus({ className, ...props }: BaseCombobox.Status.Props) {\n  return (\n    <BaseCombobox.Status\n      data-slot=\"combobox-status\"\n      className={cn(\n        \"text-muted-foreground px-3 py-2.5 text-sm leading-5 empty:m-0 empty:p-0\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction ComboboxEmpty({ className, ...props }: BaseCombobox.Empty.Props) {\n  return (\n    <BaseCombobox.Empty\n      data-slot=\"combobox-empty\"\n      className={cn(\n        \"text-muted-foreground px-3.5 py-2.5 text-sm empty:m-0 empty:p-0\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction ComboboxList({\n  className,\n  nativeScroll = false,\n  fadeEdges = true,\n  scrollbarGutter = false,\n  persistScrollbar,\n  hideScrollbar,\n  ...props\n}: BaseCombobox.List.Props &\n  Pick<\n    ScrollAreaProps,\n    | \"nativeScroll\"\n    | \"fadeEdges\"\n    | \"scrollbarGutter\"\n    | \"persistScrollbar\"\n    | \"hideScrollbar\"\n  >) {\n  return (\n    <ScrollArea\n      nativeScroll={nativeScroll}\n      fadeEdges={fadeEdges}\n      scrollbarGutter={scrollbarGutter}\n      persistScrollbar={persistScrollbar}\n      hideScrollbar={hideScrollbar}\n      className={cn(\"max-h-80\", className)}\n    >\n      <BaseCombobox.List\n        data-slot=\"combobox-list\"\n        className=\"rounded-xl\"\n        {...props}\n      />\n    </ScrollArea>\n  );\n}\n\nfunction ComboboxVirtualizedList({\n  className,\n  children,\n  scrollRef,\n  totalSize,\n  emptyMessage = \"No results found.\",\n  fadeEdges = \"y\",\n  nativeScroll = false,\n  ...props\n}: Omit<React.ComponentProps<\"div\">, \"ref\"> &\n  Pick<ScrollAreaProps, \"fadeEdges\" | \"nativeScroll\"> & {\n    scrollRef: (element: HTMLDivElement | null) => void;\n    totalSize: number;\n    emptyMessage?: React.ReactNode;\n  }) {\n  return (\n    <>\n      <BaseCombobox.Empty\n        data-slot=\"combobox-empty\"\n        className=\"text-muted-foreground px-3.5 py-2.5 text-sm empty:m-0 empty:p-0\"\n      >\n        {emptyMessage}\n      </BaseCombobox.Empty>\n      <BaseCombobox.List\n        data-slot=\"combobox-list\"\n        className=\"w-full flex-1 overflow-hidden rounded-xl p-0 outline-hidden empty:m-0 empty:p-0\"\n      >\n        <ScrollArea\n          viewportRef={scrollRef}\n          viewportClassName={cn(\"scroll-py-2\", className)}\n          fadeEdges={fadeEdges}\n          nativeScroll={nativeScroll}\n          className=\"h-auto max-h-80 w-full\"\n          {...props}\n        >\n          <div\n            role=\"presentation\"\n            className=\"relative w-full\"\n            style={{ height: totalSize }}\n          >\n            {children}\n          </div>\n        </ScrollArea>\n      </BaseCombobox.List>\n    </>\n  );\n}\n\nfunction ComboboxCollection({ ...props }: BaseCombobox.Collection.Props) {\n  return <BaseCombobox.Collection data-slot=\"combobox-collection\" {...props} />;\n}\n\nfunction ComboboxRow({ className, ...props }: BaseCombobox.Row.Props) {\n  return (\n    <BaseCombobox.Row\n      data-slot=\"combobox-row\"\n      className={cn(\"flex\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction ComboboxItem({\n  className,\n  children,\n  ref,\n  ...props\n}: BaseCombobox.Item.Props & {\n  ref?: React.Ref<HTMLDivElement>;\n}) {\n  return (\n    <BaseCombobox.Item\n      ref={ref}\n      data-slot=\"combobox-item\"\n      className={cn(\n        POPUP_ITEM,\n        \"data-highlighted:text-accent-foreground data-highlighted:bg-surface-hover relative grid cursor-default grid-cols-[1fr_1rem] items-center gap-2 pr-2 outline-none select-none data-disabled:pointer-events-none data-disabled:opacity-60\",\n        // Spacing from list edges\n        \"mx-1 first:mt-1 last:mb-1\",\n        className,\n      )}\n      {...props}\n    >\n      <div className=\"break-all\">{children}</div>\n      <BaseCombobox.ItemIndicator\n        render={\n          <HugeiconsIcon icon={CheckIcon} className=\"size-4\" strokeWidth={2} />\n        }\n      />\n    </BaseCombobox.Item>\n  );\n}\n\nfunction ComboboxItemIndicator({\n  className,\n  ...props\n}: BaseCombobox.ItemIndicator.Props) {\n  return (\n    <BaseCombobox.ItemIndicator\n      data-slot=\"combobox-item-indicator\"\n      className={cn(\"\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction ComboboxGroup({ className, ...props }: BaseCombobox.Group.Props) {\n  return (\n    <BaseCombobox.Group\n      data-slot=\"combobox-group\"\n      className={cn(\"text-foreground block\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction ComboboxGroupLabel({\n  className,\n  ...props\n}: BaseCombobox.GroupLabel.Props) {\n  return (\n    <BaseCombobox.GroupLabel\n      data-slot=\"combobox-group-label\"\n      className={cn(\n        \"text-muted-foreground bg-(--popup-surface,var(--popover)) px-3.5 py-1.5 pt-2.5 text-xs font-semibold\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction ComboboxSeparator({\n  className,\n  ...props\n}: BaseCombobox.Separator.Props) {\n  return (\n    <BaseCombobox.Separator\n      data-slot=\"combobox-separator\"\n      className={cn(\"bg-border mx-1 my-1 h-px min-h-px\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction ComboboxChips({\n  className,\n  variant = \"default\",\n  ...props\n}: BaseCombobox.Chips.Props & { variant?: \"default\" | \"elevated\" }) {\n  const context = React.use(ComboboxContext);\n\n  return (\n    <BaseCombobox.Chips\n      ref={context?.setChipsElement}\n      data-slot=\"combobox-chips\"\n      className={cn(\n        \"flex min-h-9 w-full flex-wrap items-center gap-1.5 rounded-lg border bg-clip-padding px-1.5 py-1.5\",\n        variant === \"default\" ? \"bg-input\" : \"bg-input-elevated\",\n        \"focus-within:outline-ring/50 outline-0 outline-offset-0 outline-transparent transition-[outline-width,outline-offset,outline-color] duration-100 ease-out outline-solid focus-within:outline-2 focus-within:outline-offset-2\",\n\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction ComboboxChip({ className, ...props }: BaseCombobox.Chip.Props) {\n  return (\n    <BaseCombobox.Chip\n      data-slot=\"combobox-chip\"\n      className={cn(\n        \"bg-surface-selected text-accent-foreground flex items-center gap-1 rounded-sm px-2 py-1 text-sm font-medium break-all sm:text-xs\",\n        // Ring (not outline) avoids clipping neighbors in the packed chip row;\n        // outline-none suppresses the browser default (currentColor → white in dark mode).\n        \"focus-visible:ring-ring/70 outline-none focus-visible:ring-2\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction ComboboxChipRemove({\n  className,\n  ...props\n}: BaseCombobox.ChipRemove.Props) {\n  return (\n    <BaseCombobox.ChipRemove\n      data-slot=\"combobox-chip-remove\"\n      className={cn(\n        \"ml-1 inline-flex h-4 w-4 items-center justify-center rounded-sm opacity-70 transition-opacity hover:opacity-100 disabled:pointer-events-none\",\n        \"focus-visible:ring-ring/70 cursor-pointer outline-none focus-visible:ring-2\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction ComboboxPopup({\n  className,\n  children,\n  side,\n  align,\n  sideOffset = 6,\n  alignOffset,\n  collisionBoundary,\n  collisionPadding,\n  sticky,\n  positionMethod,\n  backdrop = false,\n  level,\n  shadowLevel,\n  ...props\n}: BaseCombobox.Popup.Props & {\n  /** Which side of the anchor to align against. Defaults to Base UI's \"bottom\". */\n  side?: BaseCombobox.Positioner.Props[\"side\"];\n  /** Alignment of the popup relative to its anchor. Defaults to Base UI's \"center\". */\n  align?: BaseCombobox.Positioner.Props[\"align\"];\n  sideOffset?: BaseCombobox.Positioner.Props[\"sideOffset\"];\n  alignOffset?: BaseCombobox.Positioner.Props[\"alignOffset\"];\n  collisionBoundary?: BaseCombobox.Positioner.Props[\"collisionBoundary\"];\n  collisionPadding?: BaseCombobox.Positioner.Props[\"collisionPadding\"];\n  sticky?: BaseCombobox.Positioner.Props[\"sticky\"];\n  positionMethod?: BaseCombobox.Positioner.Props[\"positionMethod\"];\n  backdrop?: boolean;\n  /** Surface elevation level for the popup bg (1-8). Defaults to 3. */\n  level?: SurfaceLevel;\n  /** Shadow weight (1-8). Defaults to 3. */\n  shadowLevel?: SurfaceLevel;\n}) {\n  const context = React.use(ComboboxContext);\n\n  return (\n    <ComboboxPortal>\n      {backdrop && <ComboboxBackdrop />}\n      {/*\n       * Only anchor to the chips wrapper when chips are actually mounted.\n       * Otherwise pass no anchor so Base UI uses its default: the InputGroup\n       * (full field width) for a standard input, or the trigger for the\n       * input-inside-popup pattern. Forcing an anchor here would pin the popup\n       * to the narrower inner <input>.\n       */}\n      <ComboboxPositioner\n        anchor={context?.chipsElement ?? undefined}\n        side={side}\n        align={align}\n        sideOffset={sideOffset}\n        alignOffset={alignOffset}\n        collisionBoundary={collisionBoundary}\n        collisionPadding={collisionPadding}\n        sticky={sticky}\n        positionMethod={positionMethod}\n      >\n        <ComboboxPopupPrimitive\n          className={className}\n          level={level}\n          shadowLevel={shadowLevel}\n          {...props}\n        >\n          {children}\n        </ComboboxPopupPrimitive>\n      </ComboboxPositioner>\n    </ComboboxPortal>\n  );\n}\n\nfunction ComboboxLabel({\n  className,\n  ...props\n}: React.ComponentProps<typeof Label>) {\n  const context = React.use(ComboboxContext);\n\n  return <Label htmlFor={context?.id} className={className} {...props} />;\n}\n\nfunction ComboboxTriggerLabel({\n  className,\n  ...props\n}: BaseCombobox.Label.Props) {\n  return (\n    <BaseCombobox.Label\n      data-slot=\"combobox-trigger-label\"\n      className={cn(\n        \"text-foreground text-sm leading-5 font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-60 peer-disabled:cursor-not-allowed peer-disabled:opacity-60\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Combobox,\n  ComboboxInput,\n  ComboboxChipInput,\n  ComboboxTrigger,\n  ComboboxIcon,\n  ComboboxClear,\n  ComboboxValue,\n  ComboboxPortal,\n  ComboboxBackdrop,\n  ComboboxPositioner,\n  ComboboxPopupPrimitive,\n  ComboboxPopup,\n  ComboboxArrow,\n  ComboboxStatus,\n  ComboboxEmpty,\n  ComboboxList,\n  ComboboxCollection,\n  ComboboxRow,\n  ComboboxItem,\n  ComboboxItemIndicator,\n  ComboboxGroup,\n  ComboboxGroupLabel,\n  ComboboxSeparator,\n  ComboboxChips,\n  ComboboxChip,\n  ComboboxChipRemove,\n  ComboboxVirtualizedList,\n  ComboboxLabel,\n  ComboboxTriggerLabel,\n  useComboboxFilter,\n  useComboboxFilteredItems,\n  createComboboxItems,\n};\n",
      "type": "registry:ui",
      "target": "components/ui/cubby-ui/combobox/combobox.tsx"
    },
    {
      "path": "registry/default/combobox/hooks/use-async-combobox.ts",
      "content": "import * as React from \"react\";\n\nexport interface UseAsyncComboboxOptions<T extends { id: string }> {\n  /**\n   * Async search function that receives the query and an AbortSignal.\n   * Should return an array of items matching the query.\n   */\n  searchFn: (query: string, signal: AbortSignal) => Promise<T[]>;\n\n  /**\n   * Debounce delay in milliseconds. Defaults to 0 (no debounce).\n   */\n  debounceMs?: number;\n}\n\nexport interface UseAsyncComboboxSingleOptions<\n  T extends { id: string },\n> extends UseAsyncComboboxOptions<T> {\n  /**\n   * Whether multiple selection is enabled\n   */\n  multiple?: false;\n\n  /**\n   * Controlled selected value\n   */\n  value?: T | null;\n\n  /**\n   * Callback when value changes\n   */\n  onValueChange?: (value: T | null) => void;\n}\n\nexport interface UseAsyncComboboxMultipleOptions<\n  T extends { id: string },\n> extends UseAsyncComboboxOptions<T> {\n  /**\n   * Whether multiple selection is enabled\n   */\n  multiple: true;\n\n  /**\n   * Controlled selected values\n   */\n  value?: T[];\n\n  /**\n   * Callback when values change\n   */\n  onValueChange?: (value: T[]) => void;\n}\n\nexport interface UseAsyncComboboxReturn<T extends { id: string }> {\n  /**\n   * Items array with search results merged with selected values.\n   * Selected values are always kept in the list during search.\n   */\n  items: T[];\n\n  /**\n   * Props to spread onto the Combobox component\n   */\n  comboboxProps: {\n    inputValue: string;\n    onInputValueChange: (\n      value: string,\n      details: { reason: string; event: Event | React.SyntheticEvent },\n    ) => void;\n    filter: null;\n    onOpenChangeComplete: (open: boolean) => void;\n  };\n\n  /**\n   * Whether a search is in progress\n   */\n  isPending: boolean;\n\n  /**\n   * Error message if the search failed\n   */\n  error: string | null;\n\n  /**\n   * Trimmed input value for status message logic\n   */\n  query: string;\n}\n\n/**\n * Hook to manage async search combobox state and logic\n *\n * This hook encapsulates the complex logic needed for async search:\n * - AbortController for canceling in-flight requests\n * - useTransition for pending states\n * - Merging search results with selected values to keep them visible\n * - Clearing results when popup closes\n *\n * @example\n * ```tsx\n * const [value, setValue] = useState<Employee | null>(null);\n *\n * const { items, comboboxProps, isPending, error, query } = useAsyncCombobox({\n *   searchFn: searchEmployees,\n *   value,\n *   onValueChange: setValue,\n * });\n *\n * <Combobox items={items} value={value} onValueChange={setValue} {...comboboxProps}>\n *   ...\n * </Combobox>\n * ```\n */\nexport function useAsyncCombobox<T extends { id: string }>(\n  options: UseAsyncComboboxSingleOptions<T>,\n): UseAsyncComboboxReturn<T>;\nexport function useAsyncCombobox<T extends { id: string }>(\n  options: UseAsyncComboboxMultipleOptions<T>,\n): UseAsyncComboboxReturn<T>;\n// `onValueChange` is accepted (it belongs on the options object alongside\n// `value` so callers configure selection in one place) but not destructured —\n// the caller passes it to <Combobox> directly; this hook only reads `value`\n// for the selected-items merge.\nexport function useAsyncCombobox<T extends { id: string }>({\n  searchFn,\n  debounceMs = 0,\n  multiple,\n  value,\n}:\n  | UseAsyncComboboxSingleOptions<T>\n  | UseAsyncComboboxMultipleOptions<T>): UseAsyncComboboxReturn<T> {\n  const [searchResults, setSearchResults] = React.useState<T[]>([]);\n  const [inputValue, setInputValue] = React.useState(\"\");\n  const [error, setError] = React.useState<string | null>(null);\n  const [isPending, startTransition] = React.useTransition();\n\n  const abortControllerRef = React.useRef<AbortController | null>(null);\n  const debounceTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(\n    null,\n  );\n\n  const query = inputValue.trim();\n\n  // Merge search results with selected values to keep them visible\n  const items = React.useMemo(() => {\n    if (multiple) {\n      const selectedValues = (value as T[] | undefined) ?? [];\n      if (selectedValues.length === 0) {\n        return searchResults;\n      }\n      // Add selected values that aren't already in search results\n      const searchIds = new Set(searchResults.map((item) => item.id));\n      const missingSelected = selectedValues.filter(\n        (item) => !searchIds.has(item.id),\n      );\n      return [...searchResults, ...missingSelected];\n    } else {\n      const selectedValue = value as T | null | undefined;\n      if (\n        !selectedValue ||\n        searchResults.some((item) => item.id === selectedValue.id)\n      ) {\n        return searchResults;\n      }\n      return [...searchResults, selectedValue];\n    }\n  }, [searchResults, value, multiple]);\n\n  // Perform search\n  const performSearch = React.useCallback(\n    (searchQuery: string) => {\n      // Cancel any previous request\n      abortControllerRef.current?.abort();\n      const controller = new AbortController();\n      abortControllerRef.current = controller;\n\n      startTransition(async () => {\n        setError(null);\n\n        try {\n          const results = await searchFn(searchQuery, controller.signal);\n\n          if (controller.signal.aborted) {\n            return;\n          }\n\n          startTransition(() => {\n            setSearchResults(results);\n          });\n        } catch (err) {\n          if (controller.signal.aborted) {\n            return;\n          }\n\n          const message =\n            err instanceof Error ? err.message : \"Search failed. Try again.\";\n          setError(message);\n          setSearchResults([]);\n        }\n      });\n    },\n    [searchFn],\n  );\n\n  // Handle input value changes\n  const handleInputValueChange = React.useCallback(\n    (\n      nextValue: string,\n      details: { reason: string; event: Event | React.SyntheticEvent },\n    ) => {\n      setInputValue(nextValue);\n\n      // Don't search if input was cleared due to item selection\n      if (details.reason === \"item-press\") {\n        return;\n      }\n\n      // Clear debounce timer\n      if (debounceTimerRef.current) {\n        clearTimeout(debounceTimerRef.current);\n        debounceTimerRef.current = null;\n      }\n\n      const trimmed = nextValue.trim();\n\n      if (trimmed === \"\") {\n        setSearchResults([]);\n        setError(null);\n        abortControllerRef.current?.abort();\n        return;\n      }\n\n      // Debounce the search\n      if (debounceMs > 0) {\n        debounceTimerRef.current = setTimeout(() => {\n          performSearch(trimmed);\n        }, debounceMs);\n      } else {\n        performSearch(trimmed);\n      }\n    },\n    [debounceMs, performSearch],\n  );\n\n  // Handle popup close - reset to only show selected values\n  const handleOpenChangeComplete = React.useCallback(\n    (open: boolean) => {\n      if (!open) {\n        if (multiple) {\n          const selectedValues = (value as T[] | undefined) ?? [];\n          setSearchResults(selectedValues);\n          // Clear input for multiple selection (chips show the selections)\n          setInputValue(\"\");\n        } else {\n          const selectedValue = value as T | null | undefined;\n          setSearchResults(selectedValue ? [selectedValue] : []);\n          // Don't clear input for single selection - Base UI handles\n          // displaying the selected value via itemToStringLabel\n        }\n        setError(null);\n      }\n    },\n    [value, multiple],\n  );\n\n  // Cleanup on unmount\n  React.useEffect(() => {\n    return () => {\n      abortControllerRef.current?.abort();\n      if (debounceTimerRef.current) {\n        clearTimeout(debounceTimerRef.current);\n      }\n    };\n  }, []);\n\n  return {\n    items,\n    comboboxProps: {\n      inputValue,\n      onInputValueChange: handleInputValueChange,\n      filter: null,\n      onOpenChangeComplete: handleOpenChangeComplete,\n    },\n    isPending,\n    error,\n    query,\n  };\n}\n",
      "type": "registry:hook",
      "target": "components/ui/cubby-ui/combobox/hooks/use-async-combobox.ts"
    },
    {
      "path": "registry/default/combobox/hooks/use-creatable-combobox.ts",
      "content": "import * as React from \"react\";\n\nexport interface UseCreatableComboboxOptions<\n  T extends { id: string; value: string },\n> {\n  /**\n   * Controlled items for the combobox\n   */\n  items: T[];\n\n  /**\n   * Callback when items change\n   */\n  onItemsChange: (items: T[]) => void;\n\n  /**\n   * Controlled selected items\n   */\n  selectedItems: T[];\n\n  /**\n   * Callback when selected items change\n   */\n  onSelectedItemsChange: (items: T[]) => void;\n}\n\nexport interface UseCreatableComboboxReturn<\n  T extends { id: string; value: string },\n> {\n  /**\n   * Items array with pseudo \"Create X\" item injected when applicable\n   */\n  itemsWithCreatable: Array<T & { creatable?: string }>;\n\n  /**\n   * Props to spread onto the Combobox component\n   */\n  comboboxProps: {\n    value: T[];\n    onValueChange: (value: unknown) => void;\n    inputValue: string;\n    onInputValueChange: (value: string) => void;\n    onOpenChange: (\n      open: boolean,\n      details: { event: Event | React.SyntheticEvent },\n    ) => void;\n  };\n\n  /**\n   * Props to spread onto the Dialog component\n   */\n  dialogProps: {\n    open: boolean;\n    onOpenChange: (open: boolean) => void;\n  };\n\n  /**\n   * Props to spread onto the dialog input element\n   */\n  dialogInputProps: {\n    ref: React.RefObject<HTMLInputElement | null>;\n    defaultValue: string;\n  };\n\n  /**\n   * Form submit handler (handles preventDefault internally)\n   */\n  onDialogSubmit: (event: React.FormEvent<HTMLFormElement>) => void;\n\n  /**\n   * Dialog cancel handler\n   */\n  handleCancel: () => void;\n}\n\n/**\n * Slugify a string to create a valid ID\n */\nfunction slugify(value: string): string {\n  return value\n    .trim()\n    .toLowerCase()\n    .replace(/\\s+/g, \"-\")\n    .replace(/[^\\w-]/g, \"\");\n}\n\n/**\n * Generate a unique ID from a value, handling collisions\n */\nfunction generateUniqueId<T extends { id: string }>(\n  value: string,\n  existingItems: T[],\n): string {\n  const baseId = slugify(value);\n  const existingIds = new Set(existingItems.map((item) => item.id));\n\n  let uniqueId = baseId;\n  if (existingIds.has(uniqueId)) {\n    let counter = 2;\n    while (existingIds.has(`${baseId}-${counter}`)) {\n      counter += 1;\n    }\n    uniqueId = `${baseId}-${counter}`;\n  }\n\n  return uniqueId;\n}\n\n/**\n * Hook to manage creatable combobox state and logic\n *\n * This hook encapsulates all the complex logic needed for a creatable combobox:\n * - Injecting a pseudo \"Create X\" item when the query doesn't match existing items\n * - Handling item creation via a confirmation dialog with automatic ID generation\n * - Normalizing values and checking for duplicates\n *\n * @example\n * ```tsx\n * const [items, setItems] = useState(initialLabels);\n * const [selectedItems, setSelectedItems] = useState<LabelItem[]>([]);\n *\n * const { itemsWithCreatable, comboboxProps, dialogProps, dialogInputProps, onDialogSubmit, handleCancel } =\n *   useCreatableCombobox({\n *     items,\n *     onItemsChange: setItems,\n *     selectedItems,\n *     onSelectedItemsChange: setSelectedItems,\n *   });\n * ```\n */\nexport function useCreatableCombobox<T extends { id: string; value: string }>({\n  items,\n  onItemsChange,\n  selectedItems,\n  onSelectedItemsChange,\n}: UseCreatableComboboxOptions<T>): UseCreatableComboboxReturn<T> {\n  const [query, setQuery] = React.useState(\"\");\n  const [dialogOpen, setDialogOpen] = React.useState(false);\n  const [pendingValue, setPendingValue] = React.useState(\"\");\n\n  const inputRef = React.useRef<HTMLInputElement>(null);\n\n  // Helper to normalize values for comparison\n  const normalize = React.useCallback((value: string) => {\n    return value.trim().toLowerCase();\n  }, []);\n\n  // Helper to check if an item already exists\n  const itemExists = React.useCallback(\n    (value: string) => {\n      const normalized = normalize(value);\n      return items.some((item) => normalize(item.value) === normalized);\n    },\n    [items, normalize],\n  );\n\n  // Create the items array with pseudo \"Create X\" item if needed\n  const itemsWithCreatable = React.useMemo(() => {\n    const trimmed = query.trim();\n    if (!trimmed || itemExists(trimmed)) {\n      return items;\n    }\n\n    // Add pseudo item\n    const normalized = normalize(trimmed);\n    return [\n      ...items,\n      {\n        id: `create:${normalized}`,\n        value: `Create \"${trimmed}\"`,\n        creatable: trimmed,\n      } as T & { creatable: string },\n    ];\n  }, [items, query, itemExists, normalize]);\n\n  // Handle item creation\n  const handleCreate = React.useCallback(() => {\n    const value = pendingValue.trim();\n    if (!value) return;\n\n    // Check if item already exists\n    if (itemExists(value)) {\n      const existing = items.find(\n        (item) => normalize(item.value) === normalize(value),\n      );\n      if (\n        existing &&\n        !selectedItems.some((item) => item.id === existing.id)\n      ) {\n        onSelectedItemsChange([...selectedItems, existing]);\n      }\n      setDialogOpen(false);\n      setQuery(\"\");\n      setPendingValue(\"\");\n      return;\n    }\n\n    // Create new item with auto-generated ID\n    const id = generateUniqueId(value, items);\n    const newItem = { id, value } as T;\n\n    onItemsChange([...items, newItem]);\n    onSelectedItemsChange([...selectedItems, newItem]);\n    setDialogOpen(false);\n    setQuery(\"\");\n    setPendingValue(\"\");\n  }, [\n    pendingValue,\n    items,\n    selectedItems,\n    onItemsChange,\n    onSelectedItemsChange,\n    itemExists,\n    normalize,\n  ]);\n\n  // Handle value change from combobox\n  const handleValueChange = React.useCallback(\n    (value: unknown) => {\n      const valueArray = Array.isArray(value) ? value : value ? [value] : [];\n      const last = valueArray[valueArray.length - 1];\n\n      // Check if the last selected item is the pseudo \"Create X\" item\n      if (last && \"creatable\" in last && last.creatable) {\n        setPendingValue(last.creatable);\n        setDialogOpen(true);\n        return;\n      }\n\n      // Filter out any pseudo items and update selected\n      const cleanValue = valueArray.filter(\n        (item) => !(\"creatable\" in item && item.creatable),\n      );\n      onSelectedItemsChange(cleanValue as T[]);\n      setQuery(\"\");\n    },\n    [onSelectedItemsChange],\n  );\n\n  // Handle combobox open/close - intercept Enter key to open dialog\n  const handleOpenChange = React.useCallback(\n    (\n      open: boolean,\n      details: { event: Event | React.SyntheticEvent },\n    ) => {\n      // Check if Enter key was pressed with a query that doesn't match\n      if (\n        \"key\" in details.event &&\n        (details.event as KeyboardEvent).key === \"Enter\"\n      ) {\n        const trimmed = query.trim();\n        if (!trimmed) return;\n\n        // Check if item exists\n        if (itemExists(trimmed)) {\n          const existing = items.find(\n            (item) => normalize(item.value) === normalize(trimmed),\n          );\n          if (\n            existing &&\n            !selectedItems.some((item) => item.id === existing.id)\n          ) {\n            onSelectedItemsChange([...selectedItems, existing]);\n          }\n          setQuery(\"\");\n          return;\n        }\n\n        // Open dialog for new item creation\n        setPendingValue(trimmed);\n        setDialogOpen(true);\n      }\n    },\n    [query, items, selectedItems, itemExists, normalize, onSelectedItemsChange],\n  );\n\n  // Dialog submit handler\n  const onDialogSubmit = React.useCallback(\n    (event: React.FormEvent<HTMLFormElement>) => {\n      event.preventDefault();\n      handleCreate();\n    },\n    [handleCreate],\n  );\n\n  // Dialog cancel handler\n  const handleCancel = React.useCallback(() => {\n    setDialogOpen(false);\n    setPendingValue(\"\");\n  }, []);\n\n  return {\n    itemsWithCreatable,\n    comboboxProps: {\n      value: selectedItems,\n      onValueChange: handleValueChange,\n      inputValue: query,\n      onInputValueChange: setQuery,\n      onOpenChange: handleOpenChange,\n    },\n    dialogProps: {\n      open: dialogOpen,\n      onOpenChange: setDialogOpen,\n    },\n    dialogInputProps: {\n      ref: inputRef,\n      defaultValue: pendingValue,\n    },\n    onDialogSubmit,\n    handleCancel,\n  };\n}\n",
      "type": "registry:hook",
      "target": "components/ui/cubby-ui/combobox/hooks/use-creatable-combobox.ts"
    }
  ],
  "type": "registry:ui"
}