{
  "$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"
  ],
  "files": [
    {
      "path": "registry/default/combobox/combobox.tsx",
      "content": "\"use client\";\r\n\r\nimport * as React from \"react\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport {\r\n  elevatedSurface,\r\n  type SurfaceLevel,\r\n} from \"@/registry/default/lib/elevated\";\r\nimport { Label } from \"@/registry/default/label/label\";\r\nimport {\r\n  ScrollArea,\r\n  type ScrollAreaProps,\r\n} from \"@/registry/default/scroll-area/scroll-area\";\r\nimport { Combobox as BaseCombobox } from \"@base-ui/react/combobox\";\r\n\r\nimport { HugeiconsIcon } from \"@hugeicons/react\";\r\nimport {\r\n  ArrowDown01Icon,\r\n  Cancel01Icon,\r\n  Tick02Icon,\r\n} from \"@hugeicons/core-free-icons\";\r\nconst useComboboxFilter = BaseCombobox.useFilter;\r\nconst useComboboxFilteredItems = BaseCombobox.useFilteredItems;\r\n\r\n// Shared styling for the start/end addon containers. `pointer-events-none` lets\r\n// clicks on decorative content (icons, spinners) fall through to the InputGroup,\r\n// whose Base UI mousedown handler focuses the input; interactive children opt\r\n// back in so buttons/links inside an addon still receive their own clicks.\r\nconst comboboxAddonClassName = cn(\r\n  \"text-muted-foreground pointer-events-none flex shrink-0 items-center\",\r\n  \"[&_svg:not([class*='size-'])]:size-4\",\r\n  \"[&_:is(button,a,input,select,textarea,label,[role=button])]:pointer-events-auto\",\r\n);\r\n\r\nconst ComboboxContext = React.createContext<{\r\n  id: string;\r\n  /** The mounted ComboboxChips element, if any, used to anchor the popup. */\r\n  chipsElement: HTMLDivElement | null;\r\n  setChipsElement: (element: HTMLDivElement | null) => void;\r\n} | null>(null);\r\n\r\nfunction Combobox<Value, Multiple extends boolean | undefined = false>(\r\n  props: BaseCombobox.Root.Props<Value, Multiple>,\r\n): React.JSX.Element {\r\n  const id = React.useId();\r\n  const [chipsElement, setChipsElement] = React.useState<HTMLDivElement | null>(\r\n    null,\r\n  );\r\n\r\n  const contextValue = React.useMemo(\r\n    () => ({ id, chipsElement, setChipsElement }),\r\n    [id, chipsElement],\r\n  );\r\n\r\n  return (\r\n    <ComboboxContext.Provider value={contextValue}>\r\n      <BaseCombobox.Root data-slot=\"combobox\" {...props} />\r\n    </ComboboxContext.Provider>\r\n  );\r\n}\r\n\r\nfunction ComboboxInput({\r\n  id: idProp,\r\n  className,\r\n  inputClassName,\r\n  showTrigger = true,\r\n  showClear = true,\r\n  variant = \"default\",\r\n  start,\r\n  end,\r\n  ...props\r\n}: BaseCombobox.Input.Props & {\r\n  showTrigger?: boolean;\r\n  showClear?: boolean;\r\n  variant?: \"default\" | \"elevated\";\r\n  /** Content pinned to the start (leading edge) of the field, e.g. a search icon. */\r\n  start?: React.ReactNode;\r\n  /** Content pinned to the end of the field, before Clear and Trigger, e.g. a loading spinner. */\r\n  end?: React.ReactNode;\r\n  /** Class applied to the inner `<input>`. `className` styles the field wrapper. */\r\n  inputClassName?: string;\r\n}) {\r\n  const context = React.use(ComboboxContext);\r\n  const id = idProp ?? context?.id;\r\n\r\n  return (\r\n    // The wrapper carries the field chrome (border, bg, height, focus ring) so\r\n    // start/end/clear/trigger lay out as flex siblings instead of overlapping\r\n    // the input via absolute positioning + hand-tuned padding.\r\n    <BaseCombobox.InputGroup\r\n      data-slot=\"combobox-input-group\"\r\n      className={cn(\r\n        \"flex h-10 w-full min-w-0 cursor-text items-center gap-2 rounded-lg border bg-clip-padding px-3 sm:h-9\",\r\n        variant === \"default\" ? \"bg-input\" : \"bg-input-elevated\",\r\n        // Focus ring follows the input via focus-within (same pattern as ComboboxChips).\r\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\",\r\n        // Disabled state (input or field-level) dims and locks the whole field.\r\n        \"has-[input:disabled]:pointer-events-none has-[input:disabled]:cursor-not-allowed has-[input:disabled]:opacity-60\",\r\n        className,\r\n      )}\r\n    >\r\n      {start != null && (\r\n        <div\r\n          data-slot=\"combobox-input-start\"\r\n          className={comboboxAddonClassName}\r\n        >\r\n          {start}\r\n        </div>\r\n      )}\r\n      <BaseCombobox.Input\r\n        id={id}\r\n        data-slot=\"combobox-input\"\r\n        className={cn(\r\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 md:text-sm\",\r\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\",\r\n          inputClassName,\r\n        )}\r\n        {...props}\r\n      />\r\n      {end != null && (\r\n        <div data-slot=\"combobox-input-end\" className={comboboxAddonClassName}>\r\n          {end}\r\n        </div>\r\n      )}\r\n      {(showClear || showTrigger) && (\r\n        <div className=\"flex shrink-0 items-center gap-2\">\r\n          {showClear && <ComboboxClear />}\r\n          {showTrigger && <ComboboxTrigger />}\r\n        </div>\r\n      )}\r\n    </BaseCombobox.InputGroup>\r\n  );\r\n}\r\n\r\nfunction ComboboxChipInput({\r\n  id: idProp,\r\n  className,\r\n  ...props\r\n}: BaseCombobox.Input.Props) {\r\n  const context = React.use(ComboboxContext);\r\n  const id = idProp ?? context?.id;\r\n\r\n  return (\r\n    <BaseCombobox.Input\r\n      id={id}\r\n      data-slot=\"combobox-input\"\r\n      className={cn(\r\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:h-6 md:text-sm\",\r\n\r\n        className,\r\n      )}\r\n      {...props}\r\n    />\r\n  );\r\n}\r\n\r\nfunction ComboboxTrigger({\r\n  className,\r\n  children,\r\n  ...props\r\n}: BaseCombobox.Trigger.Props) {\r\n  return (\r\n    <BaseCombobox.Trigger\r\n      data-slot=\"combobox-trigger\"\r\n      aria-label=\"Open popup\"\r\n      className={cn(\r\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\",\r\n        \"focus-visible:ring-ring/70 outline-none focus-visible:ring-2\",\r\n        className,\r\n      )}\r\n      {...props}\r\n    >\r\n      {children ?? (\r\n        <HugeiconsIcon\r\n          icon={ArrowDown01Icon}\r\n          className=\"h-4 w-4\"\r\n          strokeWidth={2}\r\n        />\r\n      )}\r\n    </BaseCombobox.Trigger>\r\n  );\r\n}\r\n\r\nfunction ComboboxIcon({\r\n  className,\r\n  ...props\r\n}: React.ComponentProps<typeof BaseCombobox.Icon>) {\r\n  return (\r\n    <BaseCombobox.Icon\r\n      data-slot=\"combobox-icon\"\r\n      className={cn(\"ml-2 h-4 w-4 shrink-0 opacity-50\", className)}\r\n      {...props}\r\n    />\r\n  );\r\n}\r\n\r\nfunction ComboboxClear({ className, ...props }: BaseCombobox.Clear.Props) {\r\n  return (\r\n    <BaseCombobox.Clear\r\n      data-slot=\"combobox-clear\"\r\n      aria-label=\"Clear selection\"\r\n      className={cn(\r\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\",\r\n        \"focus-visible:ring-ring/70 duration-100 outline-none focus-visible:ring-2\",\r\n        \"data-ending-style:translate-x-1 data-ending-style:opacity-0 data-starting-style:translate-x-1 data-starting-style:opacity-0\",\r\n        className,\r\n      )}\r\n      {...props}\r\n    >\r\n      <HugeiconsIcon icon={Cancel01Icon} className=\"h-4 w-4\" strokeWidth={2} />\r\n    </BaseCombobox.Clear>\r\n  );\r\n}\r\n\r\nfunction ComboboxValue({ ...props }: BaseCombobox.Value.Props) {\r\n  return <BaseCombobox.Value data-slot=\"combobox-value\" {...props} />;\r\n}\r\n\r\nfunction ComboboxPortal({ ...props }: BaseCombobox.Portal.Props) {\r\n  return <BaseCombobox.Portal data-slot=\"combobox-portal\" {...props} />;\r\n}\r\n\r\nfunction ComboboxBackdrop({\r\n  className,\r\n  ...props\r\n}: BaseCombobox.Backdrop.Props) {\r\n  return (\r\n    <BaseCombobox.Backdrop\r\n      data-slot=\"combobox-backdrop\"\r\n      className={cn(\r\n        \"fixed inset-0 z-30 bg-black/50 data-ending-style:opacity-0 data-starting-style:opacity-0\",\r\n        className,\r\n      )}\r\n      {...props}\r\n    />\r\n  );\r\n}\r\n\r\nfunction ComboboxPositioner({\r\n  className,\r\n  ...props\r\n}: BaseCombobox.Positioner.Props) {\r\n  return (\r\n    <BaseCombobox.Positioner\r\n      data-slot=\"combobox-positioner\"\r\n      sideOffset={6}\r\n      // z-50 (matching Select's positioner) keeps the portaled popup above\r\n      // overlay surfaces it's opened from — e.g. the mobile Drawer, whose\r\n      // viewport is z-50. Without it the popup renders behind the drawer.\r\n      className={cn(\"z-50\", className)}\r\n      {...props}\r\n    />\r\n  );\r\n}\r\n\r\nfunction ComboboxPopupPrimitive({\r\n  className,\r\n  level = 3,\r\n  shadowLevel = 3,\r\n  ...props\r\n}: BaseCombobox.Popup.Props & {\r\n  /** Surface elevation level for the popup bg (1-8). Bump when nesting inside a Dialog. Defaults to 3. */\r\n  level?: SurfaceLevel;\r\n  /** Shadow weight (1-8). Pinned to 3 by default so the combobox reads the same regardless of nesting depth. */\r\n  shadowLevel?: SurfaceLevel;\r\n}) {\r\n  return (\r\n    <BaseCombobox.Popup\r\n      data-slot=\"combobox-popup\"\r\n      data-level={level}\r\n      className={cn(\r\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\",\r\n        // Use elevatedSurface (rim on ::after) because combobox group labels are\r\n        // often sticky and would otherwise hide the rim where they sit.\r\n        elevatedSurface(level, shadowLevel),\r\n        className,\r\n      )}\r\n      {...props}\r\n    />\r\n  );\r\n}\r\n\r\nfunction ComboboxArrow({ className, ...props }: BaseCombobox.Arrow.Props) {\r\n  return (\r\n    <BaseCombobox.Arrow\r\n      data-slot=\"combobox-arrow\"\r\n      className={cn(\r\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\",\r\n        className,\r\n      )}\r\n      {...props}\r\n    >\r\n      <svg width=\"20\" height=\"10\" viewBox=\"0 0 20 10\" fill=\"none\">\r\n        <path\r\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\"\r\n          className=\"fill-(--popup-surface,var(--popover))\"\r\n        />\r\n        <path\r\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\"\r\n          className=\"fill-border/70\"\r\n        />\r\n      </svg>\r\n    </BaseCombobox.Arrow>\r\n  );\r\n}\r\n\r\nfunction ComboboxStatus({ className, ...props }: BaseCombobox.Status.Props) {\r\n  return (\r\n    <BaseCombobox.Status\r\n      data-slot=\"combobox-status\"\r\n      className={cn(\r\n        \"text-muted-foreground px-3 py-2.5 text-sm leading-5 empty:m-0 empty:p-0\",\r\n        className,\r\n      )}\r\n      {...props}\r\n    />\r\n  );\r\n}\r\n\r\nfunction ComboboxEmpty({ className, ...props }: BaseCombobox.Empty.Props) {\r\n  return (\r\n    <BaseCombobox.Empty\r\n      data-slot=\"combobox-empty\"\r\n      className={cn(\r\n        \"text-muted-foreground px-3 py-2.5 text-sm empty:m-0 empty:p-0\",\r\n        className,\r\n      )}\r\n      {...props}\r\n    />\r\n  );\r\n}\r\n\r\nfunction ComboboxList({\r\n  className,\r\n  nativeScroll = false,\r\n  fadeEdges = true,\r\n  scrollbarGutter = false,\r\n  persistScrollbar,\r\n  hideScrollbar,\r\n  ...props\r\n}: BaseCombobox.List.Props &\r\n  Pick<\r\n    ScrollAreaProps,\r\n    | \"nativeScroll\"\r\n    | \"fadeEdges\"\r\n    | \"scrollbarGutter\"\r\n    | \"persistScrollbar\"\r\n    | \"hideScrollbar\"\r\n  >) {\r\n  return (\r\n    <ScrollArea\r\n      nativeScroll={nativeScroll}\r\n      fadeEdges={fadeEdges}\r\n      scrollbarGutter={scrollbarGutter}\r\n      persistScrollbar={persistScrollbar}\r\n      hideScrollbar={hideScrollbar}\r\n      className={cn(\"max-h-80\", className)}\r\n    >\r\n      <BaseCombobox.List\r\n        data-slot=\"combobox-list\"\r\n        className=\"rounded-xl\"\r\n        {...props}\r\n      />\r\n    </ScrollArea>\r\n  );\r\n}\r\n\r\nfunction ComboboxVirtualizedList({\r\n  className,\r\n  children,\r\n  scrollRef,\r\n  totalSize,\r\n  emptyMessage = \"No results found.\",\r\n  fadeEdges = \"y\",\r\n  nativeScroll = false,\r\n  ...props\r\n}: Omit<React.ComponentProps<\"div\">, \"ref\"> &\r\n  Pick<ScrollAreaProps, \"fadeEdges\" | \"nativeScroll\"> & {\r\n    scrollRef: (element: HTMLDivElement | null) => void;\r\n    totalSize: number;\r\n    emptyMessage?: React.ReactNode;\r\n  }) {\r\n  return (\r\n    <>\r\n      <BaseCombobox.Empty\r\n        data-slot=\"combobox-empty\"\r\n        className=\"text-muted-foreground px-3 py-2.5 text-sm empty:m-0 empty:p-0\"\r\n      >\r\n        {emptyMessage}\r\n      </BaseCombobox.Empty>\r\n      <BaseCombobox.List\r\n        data-slot=\"combobox-list\"\r\n        className=\"w-full flex-1 overflow-hidden rounded-xl p-0 outline-hidden empty:m-0 empty:p-0\"\r\n      >\r\n        <ScrollArea\r\n          viewportRef={scrollRef}\r\n          viewportClassName={cn(\"scroll-py-2\", className)}\r\n          fadeEdges={fadeEdges}\r\n          nativeScroll={nativeScroll}\r\n          className=\"h-auto max-h-80 w-full\"\r\n          {...props}\r\n        >\r\n          <div\r\n            role=\"presentation\"\r\n            className=\"relative w-full\"\r\n            style={{ height: totalSize }}\r\n          >\r\n            {children}\r\n          </div>\r\n        </ScrollArea>\r\n      </BaseCombobox.List>\r\n    </>\r\n  );\r\n}\r\n\r\nfunction ComboboxCollection({ ...props }: BaseCombobox.Collection.Props) {\r\n  return <BaseCombobox.Collection data-slot=\"combobox-collection\" {...props} />;\r\n}\r\n\r\nfunction ComboboxRow({ className, ...props }: BaseCombobox.Row.Props) {\r\n  return (\r\n    <BaseCombobox.Row\r\n      data-slot=\"combobox-row\"\r\n      className={cn(\"flex\", className)}\r\n      {...props}\r\n    />\r\n  );\r\n}\r\n\r\nfunction ComboboxItem({\r\n  className,\r\n  children,\r\n  ref,\r\n  ...props\r\n}: BaseCombobox.Item.Props & {\r\n  ref?: React.Ref<HTMLDivElement>;\r\n}) {\r\n  return (\r\n    <BaseCombobox.Item\r\n      ref={ref}\r\n      data-slot=\"combobox-item\"\r\n      className={cn(\r\n        \"data-highlighted:text-accent-foreground data-highlighted:bg-surface-hover relative grid cursor-default grid-cols-[1fr_1rem] items-center gap-2 rounded-md px-2.5 py-2 pr-2 text-sm outline-none select-none data-disabled:pointer-events-none data-disabled:opacity-60\",\r\n        // Spacing from list edges\r\n        \"mx-1 first:mt-1 last:mb-1\",\r\n        className,\r\n      )}\r\n      {...props}\r\n    >\r\n      <div className=\"break-all\">{children}</div>\r\n      <BaseCombobox.ItemIndicator\r\n        render={\r\n          <HugeiconsIcon icon={Tick02Icon} className=\"size-4\" strokeWidth={2} />\r\n        }\r\n      />\r\n    </BaseCombobox.Item>\r\n  );\r\n}\r\n\r\nfunction ComboboxItemIndicator({\r\n  className,\r\n  ...props\r\n}: BaseCombobox.ItemIndicator.Props) {\r\n  return (\r\n    <BaseCombobox.ItemIndicator\r\n      data-slot=\"combobox-item-indicator\"\r\n      className={cn(\"\", className)}\r\n      {...props}\r\n    />\r\n  );\r\n}\r\n\r\nfunction ComboboxGroup({ className, ...props }: BaseCombobox.Group.Props) {\r\n  return (\r\n    <BaseCombobox.Group\r\n      data-slot=\"combobox-group\"\r\n      className={cn(\"text-foreground block\", className)}\r\n      {...props}\r\n    />\r\n  );\r\n}\r\n\r\nfunction ComboboxGroupLabel({\r\n  className,\r\n  ...props\r\n}: BaseCombobox.GroupLabel.Props) {\r\n  return (\r\n    <BaseCombobox.GroupLabel\r\n      data-slot=\"combobox-group-label\"\r\n      className={cn(\r\n        \"text-muted-foreground bg-(--popup-surface,var(--popover)) px-3.5 py-1.5 pt-2.5 text-xs font-semibold\",\r\n        className,\r\n      )}\r\n      {...props}\r\n    />\r\n  );\r\n}\r\n\r\nfunction ComboboxSeparator({\r\n  className,\r\n  ...props\r\n}: BaseCombobox.Separator.Props) {\r\n  return (\r\n    <BaseCombobox.Separator\r\n      data-slot=\"combobox-separator\"\r\n      className={cn(\"bg-border mx-1 my-1 h-px min-h-px\", className)}\r\n      {...props}\r\n    />\r\n  );\r\n}\r\n\r\nfunction ComboboxChips({\r\n  className,\r\n  variant = \"default\",\r\n  ...props\r\n}: BaseCombobox.Chips.Props & { variant?: \"default\" | \"elevated\" }) {\r\n  const context = React.use(ComboboxContext);\r\n\r\n  return (\r\n    <BaseCombobox.Chips\r\n      ref={context?.setChipsElement}\r\n      data-slot=\"combobox-chips\"\r\n      className={cn(\r\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\",\r\n        variant === \"default\" ? \"bg-input\" : \"bg-input-elevated\",\r\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\",\r\n\r\n        className,\r\n      )}\r\n      {...props}\r\n    />\r\n  );\r\n}\r\n\r\nfunction ComboboxChip({ className, ...props }: BaseCombobox.Chip.Props) {\r\n  return (\r\n    <BaseCombobox.Chip\r\n      data-slot=\"combobox-chip\"\r\n      className={cn(\r\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\",\r\n        // Ring (not outline) avoids clipping neighbors in the packed chip row;\r\n        // outline-none suppresses the browser default (currentColor → white in dark mode).\r\n        \"focus-visible:ring-ring/70 outline-none focus-visible:ring-2\",\r\n        className,\r\n      )}\r\n      {...props}\r\n    />\r\n  );\r\n}\r\n\r\nfunction ComboboxChipRemove({\r\n  className,\r\n  ...props\r\n}: BaseCombobox.ChipRemove.Props) {\r\n  return (\r\n    <BaseCombobox.ChipRemove\r\n      data-slot=\"combobox-chip-remove\"\r\n      className={cn(\r\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\",\r\n        \"focus-visible:ring-ring/70 cursor-pointer outline-none focus-visible:ring-2\",\r\n        className,\r\n      )}\r\n      {...props}\r\n    />\r\n  );\r\n}\r\n\r\nfunction ComboboxPopup({\r\n  className,\r\n  children,\r\n  side,\r\n  align,\r\n  sideOffset = 6,\r\n  alignOffset,\r\n  collisionBoundary,\r\n  collisionPadding,\r\n  sticky,\r\n  positionMethod,\r\n  backdrop = false,\r\n  level,\r\n  shadowLevel,\r\n  ...props\r\n}: BaseCombobox.Popup.Props & {\r\n  /** Which side of the anchor to align against. Defaults to Base UI's \"bottom\". */\r\n  side?: BaseCombobox.Positioner.Props[\"side\"];\r\n  /** Alignment of the popup relative to its anchor. Defaults to Base UI's \"center\". */\r\n  align?: BaseCombobox.Positioner.Props[\"align\"];\r\n  sideOffset?: BaseCombobox.Positioner.Props[\"sideOffset\"];\r\n  alignOffset?: BaseCombobox.Positioner.Props[\"alignOffset\"];\r\n  collisionBoundary?: BaseCombobox.Positioner.Props[\"collisionBoundary\"];\r\n  collisionPadding?: BaseCombobox.Positioner.Props[\"collisionPadding\"];\r\n  sticky?: BaseCombobox.Positioner.Props[\"sticky\"];\r\n  positionMethod?: BaseCombobox.Positioner.Props[\"positionMethod\"];\r\n  backdrop?: boolean;\r\n  /** Surface elevation level for the popup bg (1-8). Defaults to 3. */\r\n  level?: SurfaceLevel;\r\n  /** Shadow weight (1-8). Defaults to 3. */\r\n  shadowLevel?: SurfaceLevel;\r\n}) {\r\n  const context = React.use(ComboboxContext);\r\n\r\n  return (\r\n    <ComboboxPortal>\r\n      {backdrop && <ComboboxBackdrop />}\r\n      {/*\r\n       * Only anchor to the chips wrapper when chips are actually mounted.\r\n       * Otherwise pass no anchor so Base UI uses its default: the InputGroup\r\n       * (full field width) for a standard input, or the trigger for the\r\n       * input-inside-popup pattern. Forcing an anchor here would pin the popup\r\n       * to the narrower inner <input>.\r\n       */}\r\n      <ComboboxPositioner\r\n        anchor={context?.chipsElement ?? undefined}\r\n        side={side}\r\n        align={align}\r\n        sideOffset={sideOffset}\r\n        alignOffset={alignOffset}\r\n        collisionBoundary={collisionBoundary}\r\n        collisionPadding={collisionPadding}\r\n        sticky={sticky}\r\n        positionMethod={positionMethod}\r\n      >\r\n        <ComboboxPopupPrimitive\r\n          className={className}\r\n          level={level}\r\n          shadowLevel={shadowLevel}\r\n          {...props}\r\n        >\r\n          {children}\r\n        </ComboboxPopupPrimitive>\r\n      </ComboboxPositioner>\r\n    </ComboboxPortal>\r\n  );\r\n}\r\n\r\nfunction ComboboxLabel({\r\n  className,\r\n  ...props\r\n}: React.ComponentProps<typeof Label>) {\r\n  const context = React.use(ComboboxContext);\r\n\r\n  return <Label htmlFor={context?.id} className={className} {...props} />;\r\n}\r\n\r\nfunction ComboboxTriggerLabel({\r\n  className,\r\n  ...props\r\n}: BaseCombobox.Label.Props) {\r\n  return (\r\n    <BaseCombobox.Label\r\n      data-slot=\"combobox-trigger-label\"\r\n      className={cn(\r\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\",\r\n        className,\r\n      )}\r\n      {...props}\r\n    />\r\n  );\r\n}\r\n\r\nexport {\r\n  Combobox,\r\n  ComboboxInput,\r\n  ComboboxChipInput,\r\n  ComboboxTrigger,\r\n  ComboboxIcon,\r\n  ComboboxClear,\r\n  ComboboxValue,\r\n  ComboboxPortal,\r\n  ComboboxBackdrop,\r\n  ComboboxPositioner,\r\n  ComboboxPopupPrimitive,\r\n  ComboboxPopup,\r\n  ComboboxArrow,\r\n  ComboboxStatus,\r\n  ComboboxEmpty,\r\n  ComboboxList,\r\n  ComboboxCollection,\r\n  ComboboxRow,\r\n  ComboboxItem,\r\n  ComboboxItemIndicator,\r\n  ComboboxGroup,\r\n  ComboboxGroupLabel,\r\n  ComboboxSeparator,\r\n  ComboboxChips,\r\n  ComboboxChip,\r\n  ComboboxChipRemove,\r\n  ComboboxVirtualizedList,\r\n  ComboboxLabel,\r\n  ComboboxTriggerLabel,\r\n  useComboboxFilter,\r\n  useComboboxFilteredItems,\r\n};\r\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\";\r\n\r\nexport interface UseAsyncComboboxOptions<T extends { id: string }> {\r\n  /**\r\n   * Async search function that receives the query and an AbortSignal.\r\n   * Should return an array of items matching the query.\r\n   */\r\n  searchFn: (query: string, signal: AbortSignal) => Promise<T[]>;\r\n\r\n  /**\r\n   * Debounce delay in milliseconds. Defaults to 0 (no debounce).\r\n   */\r\n  debounceMs?: number;\r\n}\r\n\r\nexport interface UseAsyncComboboxSingleOptions<\r\n  T extends { id: string },\r\n> extends UseAsyncComboboxOptions<T> {\r\n  /**\r\n   * Whether multiple selection is enabled\r\n   */\r\n  multiple?: false;\r\n\r\n  /**\r\n   * Controlled selected value\r\n   */\r\n  value?: T | null;\r\n\r\n  /**\r\n   * Callback when value changes\r\n   */\r\n  onValueChange?: (value: T | null) => void;\r\n}\r\n\r\nexport interface UseAsyncComboboxMultipleOptions<\r\n  T extends { id: string },\r\n> extends UseAsyncComboboxOptions<T> {\r\n  /**\r\n   * Whether multiple selection is enabled\r\n   */\r\n  multiple: true;\r\n\r\n  /**\r\n   * Controlled selected values\r\n   */\r\n  value?: T[];\r\n\r\n  /**\r\n   * Callback when values change\r\n   */\r\n  onValueChange?: (value: T[]) => void;\r\n}\r\n\r\nexport interface UseAsyncComboboxReturn<T extends { id: string }> {\r\n  /**\r\n   * Items array with search results merged with selected values.\r\n   * Selected values are always kept in the list during search.\r\n   */\r\n  items: T[];\r\n\r\n  /**\r\n   * Props to spread onto the Combobox component\r\n   */\r\n  comboboxProps: {\r\n    inputValue: string;\r\n    onInputValueChange: (\r\n      value: string,\r\n      details: { reason: string; event: Event | React.SyntheticEvent },\r\n    ) => void;\r\n    filter: null;\r\n    onOpenChangeComplete: (open: boolean) => void;\r\n  };\r\n\r\n  /**\r\n   * Whether a search is in progress\r\n   */\r\n  isPending: boolean;\r\n\r\n  /**\r\n   * Error message if the search failed\r\n   */\r\n  error: string | null;\r\n\r\n  /**\r\n   * Trimmed input value for status message logic\r\n   */\r\n  query: string;\r\n}\r\n\r\n/**\r\n * Hook to manage async search combobox state and logic\r\n *\r\n * This hook encapsulates the complex logic needed for async search:\r\n * - AbortController for canceling in-flight requests\r\n * - useTransition for pending states\r\n * - Merging search results with selected values to keep them visible\r\n * - Clearing results when popup closes\r\n *\r\n * @example\r\n * ```tsx\r\n * const [value, setValue] = useState<Employee | null>(null);\r\n *\r\n * const { items, comboboxProps, isPending, error, query } = useAsyncCombobox({\r\n *   searchFn: searchEmployees,\r\n *   value,\r\n *   onValueChange: setValue,\r\n * });\r\n *\r\n * <Combobox items={items} value={value} onValueChange={setValue} {...comboboxProps}>\r\n *   ...\r\n * </Combobox>\r\n * ```\r\n */\r\nexport function useAsyncCombobox<T extends { id: string }>(\r\n  options: UseAsyncComboboxSingleOptions<T>,\r\n): UseAsyncComboboxReturn<T>;\r\nexport function useAsyncCombobox<T extends { id: string }>(\r\n  options: UseAsyncComboboxMultipleOptions<T>,\r\n): UseAsyncComboboxReturn<T>;\r\n// `onValueChange` is accepted (it belongs on the options object alongside\r\n// `value` so callers configure selection in one place) but not destructured —\r\n// the caller passes it to <Combobox> directly; this hook only reads `value`\r\n// for the selected-items merge.\r\nexport function useAsyncCombobox<T extends { id: string }>({\r\n  searchFn,\r\n  debounceMs = 0,\r\n  multiple,\r\n  value,\r\n}:\r\n  | UseAsyncComboboxSingleOptions<T>\r\n  | UseAsyncComboboxMultipleOptions<T>): UseAsyncComboboxReturn<T> {\r\n  const [searchResults, setSearchResults] = React.useState<T[]>([]);\r\n  const [inputValue, setInputValue] = React.useState(\"\");\r\n  const [error, setError] = React.useState<string | null>(null);\r\n  const [isPending, startTransition] = React.useTransition();\r\n\r\n  const abortControllerRef = React.useRef<AbortController | null>(null);\r\n  const debounceTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(\r\n    null,\r\n  );\r\n\r\n  const query = inputValue.trim();\r\n\r\n  // Merge search results with selected values to keep them visible\r\n  const items = React.useMemo(() => {\r\n    if (multiple) {\r\n      const selectedValues = (value as T[] | undefined) ?? [];\r\n      if (selectedValues.length === 0) {\r\n        return searchResults;\r\n      }\r\n      // Add selected values that aren't already in search results\r\n      const searchIds = new Set(searchResults.map((item) => item.id));\r\n      const missingSelected = selectedValues.filter(\r\n        (item) => !searchIds.has(item.id),\r\n      );\r\n      return [...searchResults, ...missingSelected];\r\n    } else {\r\n      const selectedValue = value as T | null | undefined;\r\n      if (\r\n        !selectedValue ||\r\n        searchResults.some((item) => item.id === selectedValue.id)\r\n      ) {\r\n        return searchResults;\r\n      }\r\n      return [...searchResults, selectedValue];\r\n    }\r\n  }, [searchResults, value, multiple]);\r\n\r\n  // Perform search\r\n  const performSearch = React.useCallback(\r\n    (searchQuery: string) => {\r\n      // Cancel any previous request\r\n      abortControllerRef.current?.abort();\r\n      const controller = new AbortController();\r\n      abortControllerRef.current = controller;\r\n\r\n      startTransition(async () => {\r\n        setError(null);\r\n\r\n        try {\r\n          const results = await searchFn(searchQuery, controller.signal);\r\n\r\n          if (controller.signal.aborted) {\r\n            return;\r\n          }\r\n\r\n          startTransition(() => {\r\n            setSearchResults(results);\r\n          });\r\n        } catch (err) {\r\n          if (controller.signal.aborted) {\r\n            return;\r\n          }\r\n\r\n          const message =\r\n            err instanceof Error ? err.message : \"Search failed. Try again.\";\r\n          setError(message);\r\n          setSearchResults([]);\r\n        }\r\n      });\r\n    },\r\n    [searchFn],\r\n  );\r\n\r\n  // Handle input value changes\r\n  const handleInputValueChange = React.useCallback(\r\n    (\r\n      nextValue: string,\r\n      details: { reason: string; event: Event | React.SyntheticEvent },\r\n    ) => {\r\n      setInputValue(nextValue);\r\n\r\n      // Don't search if input was cleared due to item selection\r\n      if (details.reason === \"item-press\") {\r\n        return;\r\n      }\r\n\r\n      // Clear debounce timer\r\n      if (debounceTimerRef.current) {\r\n        clearTimeout(debounceTimerRef.current);\r\n        debounceTimerRef.current = null;\r\n      }\r\n\r\n      const trimmed = nextValue.trim();\r\n\r\n      if (trimmed === \"\") {\r\n        setSearchResults([]);\r\n        setError(null);\r\n        abortControllerRef.current?.abort();\r\n        return;\r\n      }\r\n\r\n      // Debounce the search\r\n      if (debounceMs > 0) {\r\n        debounceTimerRef.current = setTimeout(() => {\r\n          performSearch(trimmed);\r\n        }, debounceMs);\r\n      } else {\r\n        performSearch(trimmed);\r\n      }\r\n    },\r\n    [debounceMs, performSearch],\r\n  );\r\n\r\n  // Handle popup close - reset to only show selected values\r\n  const handleOpenChangeComplete = React.useCallback(\r\n    (open: boolean) => {\r\n      if (!open) {\r\n        if (multiple) {\r\n          const selectedValues = (value as T[] | undefined) ?? [];\r\n          setSearchResults(selectedValues);\r\n          // Clear input for multiple selection (chips show the selections)\r\n          setInputValue(\"\");\r\n        } else {\r\n          const selectedValue = value as T | null | undefined;\r\n          setSearchResults(selectedValue ? [selectedValue] : []);\r\n          // Don't clear input for single selection - Base UI handles\r\n          // displaying the selected value via itemToStringLabel\r\n        }\r\n        setError(null);\r\n      }\r\n    },\r\n    [value, multiple],\r\n  );\r\n\r\n  // Cleanup on unmount\r\n  React.useEffect(() => {\r\n    return () => {\r\n      abortControllerRef.current?.abort();\r\n      if (debounceTimerRef.current) {\r\n        clearTimeout(debounceTimerRef.current);\r\n      }\r\n    };\r\n  }, []);\r\n\r\n  return {\r\n    items,\r\n    comboboxProps: {\r\n      inputValue,\r\n      onInputValueChange: handleInputValueChange,\r\n      filter: null,\r\n      onOpenChangeComplete: handleOpenChangeComplete,\r\n    },\r\n    isPending,\r\n    error,\r\n    query,\r\n  };\r\n}\r\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"
}