{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "combobox",
  "title": "Combobox",
  "dependencies": [
    "@base-ui/react",
    "@phosphor-icons/react",
    "react-remove-scroll"
  ],
  "registryDependencies": [
    "https://ui.hotfix.jobs/r/checkbox.json",
    "https://ui.hotfix.jobs/r/input.json",
    "https://ui.hotfix.jobs/r/recipes.json",
    "https://ui.hotfix.jobs/r/tokens.json",
    "https://ui.hotfix.jobs/r/use-media-query.json",
    "https://ui.hotfix.jobs/r/utils.json"
  ],
  "files": [
    {
      "path": "registry/components/ui/combobox.tsx",
      "content": "\"use client\";\n\nimport { Combobox as ComboboxPrimitive } from \"@base-ui/react/combobox\";\nimport {\n  CaretDown,\n  MagnifyingGlass,\n  X,\n} from \"@phosphor-icons/react/dist/ssr\";\nimport { RemoveScroll } from \"react-remove-scroll\";\nimport { createContext, forwardRef, useContext, useMemo } from \"react\";\nimport type * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  itemGroupLabel,\n  itemRow,\n  mobilePopupBackdrop,\n  mobilePopupSurface,\n  popupDivider,\n  popupSurface,\n  popupTriggerOpen,\n} from \"@/lib/recipes\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport type { InputSize } from \"@/components/ui/input\";\nimport {\n  MOBILE_MEDIA_QUERY,\n  useMediaQuery,\n} from \"../hooks/use-media-query\";\n\n/* --------------------------------- Root -------------------------------- */\n\nexport interface ComboboxProps {\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  /** Search text. */\n  value?: string;\n  onValueChange?: (next: string) => void;\n  placeholder?: string;\n  /**\n   * Enables Base UI's multi-select mode. When `true`, `selectedValues`\n   * is the source of truth for the picked items and the built-in\n   * ComboboxInput clearable X becomes visible whenever the array is\n   * non-empty. Clicking Clear then wipes both the input value and the\n   * selection array via Base UI's native handler.\n   */\n  multiple?: boolean;\n  /** Selected values in multi-select mode (Base UI's `value` array). */\n  selectedValues?: readonly unknown[];\n  /** Fires when Base UI toggles the selection array in multi mode. */\n  onSelectedValuesChange?: (next: unknown[]) => void;\n  /**\n   * Auto-highlight behavior on the popup list:\n   *   - `false` (default): open with no highlighted item. Arrow keys move to\n   *     an option before Enter can select it.\n   *   - `true`: only highlight on input change, not on plain open.\n   *   - `\"always\"`: highlight the first item on open and input change.\n   */\n  autoHighlight?: boolean | \"always\";\n  children: React.ReactNode;\n}\n\nconst ComboboxSharedContext = createContext<{\n  placeholder?: string;\n  multiple?: boolean;\n  isMobile: boolean;\n}>({ isMobile: false });\n\nexport function Combobox({\n  open,\n  defaultOpen,\n  onOpenChange,\n  value,\n  onValueChange,\n  placeholder,\n  multiple,\n  selectedValues,\n  onSelectedValuesChange,\n  autoHighlight = false,\n  children,\n}: ComboboxProps): React.ReactElement {\n  const isMobile = useMediaQuery(MOBILE_MEDIA_QUERY);\n  const shared = useMemo(\n    () => ({ placeholder, multiple, isMobile }),\n    [placeholder, multiple, isMobile],\n  );\n  // Base UI's ComboboxRoot is generic over Value + Multiple, and the\n  // shape of `value` depends on both. Cast through `unknown as never` at\n  // the boundary so consumers can pass a `string[]` selection array\n  // without threading generics through our wrapper.\n  const rootProps = multiple\n    ? {\n        multiple: true as const,\n        value: selectedValues as never,\n        onValueChange: onSelectedValuesChange\n          ? (next: unknown) =>\n              onSelectedValuesChange((next as unknown[]) ?? [])\n          : undefined,\n      }\n    : {};\n\n  return (\n    <ComboboxSharedContext.Provider value={shared}>\n      <ComboboxPrimitive.Root\n        {...rootProps}\n        open={open}\n        defaultOpen={defaultOpen}\n        onOpenChange={onOpenChange}\n        inputValue={value}\n        onInputValueChange={\n          onValueChange ? (next) => onValueChange(next) : undefined\n        }\n        // Base UI's ComboboxRoot narrows autoHighlight to boolean in its\n        // public types, but the underlying AriaCombobox accepts 'always'\n        // and forwards it unchanged. Cast so consumers can opt into the\n        // stronger mode without dropping to the primitive.\n        autoHighlight={autoHighlight as boolean}\n        modal={isMobile}\n      >\n        {children}\n      </ComboboxPrimitive.Root>\n    </ComboboxSharedContext.Provider>\n  );\n}\n\n/* -------------------------------- Input -------------------------------- */\n\nfunction ChevronIndicator({\n  className,\n}: {\n  className?: string;\n}): React.ReactElement {\n  return (\n    <ComboboxPrimitive.Icon\n      render={\n        <CaretDown\n          aria-hidden\n          className={cn(\n            \"size-4 shrink-0 text-ink-muted transition-transform duration-[var(--duration-state)] ease-[var(--ease-standard)]\",\n            className,\n          )}\n        />\n      }\n    />\n  );\n}\n\nexport interface ComboboxInputProps\n  extends Omit<React.InputHTMLAttributes<HTMLInputElement>, \"size\" | \"prefix\"> {\n  size?: InputSize;\n  /** Removes control chrome when embedded in a composite surface. */\n  variant?: \"default\" | \"unstyled\";\n  prefix?: React.ReactNode;\n  suffix?: React.ReactNode;\n  /** Hide the trailing chevron. */\n  hideChevron?: boolean;\n  /** Show a trailing X to clear the input. */\n  clearable?: boolean;\n  onClear?: () => void;\n}\n\nconst heightBySize: Record<InputSize, string> = {\n  sm: \"h-6 text-small\",\n  md: \"h-8 text-small\",\n  lg: \"h-10 text-regular\",\n};\n\nconst startPadBySize: Record<InputSize, string> = {\n  sm: \"ps-3\",\n  md: \"ps-3\",\n  lg: \"ps-3.5\",\n};\n\nconst endPadBySize: Record<InputSize, string> = {\n  sm: \"pe-3\",\n  md: \"pe-3\",\n  lg: \"pe-3.5\",\n};\n\nfunction ComboboxAffix({\n  side,\n  children,\n}: {\n  side: \"start\" | \"end\";\n  children: React.ReactNode;\n}) {\n  return (\n    <span\n      className={cn(\n        \"inline-flex shrink-0 items-center text-ink-muted\",\n        side === \"start\" ? \"ps-3 pe-1.5\" : \"ps-1.5 pe-3\",\n        \"[&_svg]:size-4\",\n      )}\n      data-slot={`combobox-input-${side === \"start\" ? \"prefix\" : \"suffix\"}`}\n    >\n      {children}\n    </span>\n  );\n}\n\nexport const ComboboxInput = forwardRef<HTMLInputElement, ComboboxInputProps>(\n  function ComboboxInput(\n    {\n      size = \"md\",\n      variant = \"default\",\n      prefix,\n      suffix,\n      hideChevron,\n      clearable,\n      onClear,\n      disabled,\n      className,\n      ...props\n    },\n    forwardedRef,\n  ) {\n    const unstyled = variant === \"unstyled\";\n    const trailing = (clearable || !hideChevron || suffix) ? (\n      <ComboboxAffix side=\"end\">\n        <span className={cn(\"inline-flex items-center gap-1.5\", clearable && \"group\")}>\n          {suffix}\n          {clearable && (\n            <ComboboxPrimitive.Clear\n              aria-label=\"Clear\"\n              onClick={onClear}\n              data-slot=\"combobox-input-clear\"\n              className=\"inline-flex size-5 items-center justify-center rounded-[var(--radius-8)] text-ink-muted hover:bg-layer-hover hover:text-ink active:bg-layer-hover transition-colors duration-[var(--duration-state)] ease-[var(--ease-standard)] data-[ending-style]:hidden\"\n            >\n              <X className=\"size-3\" />\n            </ComboboxPrimitive.Clear>\n          )}\n          {!hideChevron && (\n            <ChevronIndicator\n              className={cn(\n                \"group-data-[popup-open]/combobox-input:rotate-180\",\n                clearable &&\n                  \"group-has-[[data-slot=combobox-input-clear]:not([data-ending-style])]:hidden\",\n              )}\n            />\n          )}\n        </span>\n      </ComboboxAffix>\n    ) : null;\n\n    return (\n      <ComboboxPrimitive.InputGroup\n        data-slot=\"combobox-input-group\"\n        className={cn(\n          \"group/combobox-input relative inline-flex w-full items-center overflow-hidden text-ink\",\n          !unstyled && \"rounded-[var(--radius-8)]\",\n          !unstyled && [\n            \"bg-fill-1 hover:bg-fill-2\",\n            popupTriggerOpen,\n            \"outline-none has-focus-visible:[outline-style:solid] has-focus-visible:outline-[length:var(--focus-ring-width)] has-focus-visible:outline-[var(--focus-ring-color)] has-focus-visible:outline-offset-0\",\n            \"transition-[color,background-color,outline-color] duration-[var(--duration-state)] ease-[var(--ease-standard)]\",\n          ],\n          \"has-disabled:opacity-50 has-disabled:cursor-not-allowed\",\n          \"group-data-[invalid]/field:[outline-style:solid] group-data-[invalid]/field:outline-[length:1px] group-data-[invalid]/field:outline-error\",\n          \"has-[[aria-invalid=true]]:[outline-style:solid] has-[[aria-invalid=true]]:outline-[length:1px] has-[[aria-invalid=true]]:outline-error\",\n          className,\n        )}\n      >\n        {prefix && <ComboboxAffix side=\"start\">{prefix}</ComboboxAffix>}\n        <ComboboxPrimitive.Input\n          ref={forwardedRef}\n          disabled={disabled}\n          autoComplete=\"off\"\n          data-slot=\"combobox-input\"\n          className={cn(\n            \"w-full min-w-0 bg-transparent border-none shadow-none outline-none ring-0\",\n            \"placeholder:text-ink-subtle focus:outline-none focus:ring-0\",\n            heightBySize[size],\n            prefix ? \"ps-0\" : startPadBySize[size],\n            trailing ? \"pe-0\" : endPadBySize[size],\n          )}\n          {...props}\n        />\n        {trailing}\n      </ComboboxPrimitive.InputGroup>\n    );\n  },\n);\n\n/* -------------------------------- Popup -------------------------------- */\n\nexport interface ComboboxPopupProps {\n  /** Applies to whichever popup renders (desktop or mobile).\n   *  Use for styling that makes sense in both contexts. */\n  className?: string;\n  /** Desktop-only overrides. Trigger-anchored width lives here\n   *  (e.g. `w-[var(--anchor-width)]`) — it's meaningless for the\n   *  centered mobile modal, so we drop it on mobile. */\n  desktopClassName?: string;\n  /** Mobile-only overrides for the centered modal layout. */\n  mobileClassName?: string;\n  /** Desktop placement. Mobile uses a centered modal surface. */\n  side?: \"top\" | \"bottom\" | \"left\" | \"right\";\n  /** Desktop alignment. Mobile uses a centered modal surface. */\n  align?: \"start\" | \"center\" | \"end\";\n  /** Desktop trigger offset. Mobile uses viewport gutters. */\n  sideOffset?: number;\n  children: React.ReactNode;\n}\n\nexport function ComboboxPopup({\n  className,\n  desktopClassName,\n  mobileClassName,\n  side = \"bottom\",\n  align = \"start\",\n  sideOffset = 6,\n  children,\n}: ComboboxPopupProps): React.ReactElement {\n  const { placeholder, isMobile } = useContext(ComboboxSharedContext);\n\n  if (isMobile) {\n    return (\n      <ComboboxPrimitive.Portal>\n        <RemoveScroll>\n        <ComboboxPrimitive.Backdrop\n          data-slot=\"combobox-backdrop\"\n          className={mobilePopupBackdrop}\n        />\n        <ComboboxPrimitive.Positioner className=\"contents\">\n          <ComboboxPrimitive.Popup\n            data-slot=\"combobox-popup\"\n            data-mobile=\"true\"\n            className={cn(\n              mobilePopupSurface,\n              className,\n              mobileClassName,\n            )}\n          >\n            <ComboboxPrimitive.InputGroup\n              data-slot=\"combobox-mobile-input\"\n              className=\"flex-none flex w-full items-center gap-2 px-3 text-ink\"\n            >\n              <MagnifyingGlass\n                aria-hidden\n                className=\"size-4 shrink-0 text-ink-muted\"\n              />\n              <ComboboxPrimitive.Input\n                autoComplete=\"off\"\n                placeholder={placeholder}\n                className=\"h-11 w-full min-w-0 bg-transparent border-none shadow-none outline-none ring-0 placeholder:text-ink-subtle focus:outline-none focus:ring-0 text-regular\"\n              />\n            </ComboboxPrimitive.InputGroup>\n            <ComboboxPrimitive.List className=\"min-h-0 flex-1 overflow-y-auto p-1\">\n              {children}\n            </ComboboxPrimitive.List>\n          </ComboboxPrimitive.Popup>\n        </ComboboxPrimitive.Positioner>\n        </RemoveScroll>\n      </ComboboxPrimitive.Portal>\n    );\n  }\n\n  return (\n    <ComboboxPrimitive.Portal>\n      <ComboboxPrimitive.Positioner\n        side={side}\n        align={align}\n        sideOffset={sideOffset}\n        className=\"z-[80] outline-none\"\n      >\n        <ComboboxPrimitive.Popup\n          data-slot=\"combobox-popup\"\n          className={cn(\n            \"flex flex-col overflow-hidden outline-none\",\n            popupSurface,\n            \"min-w-[var(--anchor-width)] max-h-[min(var(--available-height),400px)]\",\n            \"transition-[opacity,scale] duration-[var(--duration-state)] ease-[var(--ease-standard)]\",\n            \"data-starting-style:opacity-0 data-starting-style:scale-95\",\n            \"data-ending-style:opacity-0 data-ending-style:scale-95\",\n            className,\n            desktopClassName,\n          )}\n        >\n          <ComboboxPrimitive.List className=\"min-h-0 flex-1 overflow-y-auto p-1\">\n            {children}\n          </ComboboxPrimitive.List>\n        </ComboboxPrimitive.Popup>\n      </ComboboxPrimitive.Positioner>\n    </ComboboxPrimitive.Portal>\n  );\n}\n\n/* --------------------------------- Item -------------------------------- */\n\nexport interface ComboboxItemProps\n  extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onSelect\"> {\n  onSelect?: () => void;\n  disabled?: boolean;\n  value?: unknown;\n  /** When provided, renders a trailing X affordance that calls this\n   *  callback instead of Base UI's select handler on click. Useful in\n   *  multi-select Selected sections where every row already represents\n   *  an active pick and clicking the row still toggles it off, but a\n   *  visible X communicates that intent more clearly. */\n  onRemove?: () => void;\n  /** aria-label for the remove X. Defaults to \"Remove\". */\n  removeLabel?: string;\n}\n\nexport function ComboboxItem({\n  className,\n  children,\n  onSelect,\n  disabled,\n  value,\n  onClick,\n  onRemove,\n  removeLabel = \"Remove\",\n  ...props\n}: ComboboxItemProps): React.ReactElement {\n  const { multiple } = useContext(ComboboxSharedContext);\n  const resolvedValue =\n    value !== undefined ? value : typeof children === \"string\" ? children : null;\n  return (\n    <ComboboxPrimitive.Item\n      value={resolvedValue}\n      disabled={disabled}\n      data-slot=\"combobox-item\"\n      className={cn(\n        \"group\",\n        itemRow.base,\n        itemRow.comfortable,\n        \"gap-2 transition-colors duration-[var(--duration-state)] ease-[var(--ease-standard)]\",\n        className,\n      )}\n      onClick={(e: React.MouseEvent<HTMLDivElement>) => {\n        onClick?.(e);\n        if (e.defaultPrevented) return;\n        onSelect?.();\n      }}\n      {...props}\n    >\n      {multiple && (\n        <span\n          aria-hidden\n          data-slot=\"combobox-item-checkbox\"\n          className=\"relative inline-flex size-4 shrink-0 items-center justify-center\"\n        >\n          <Checkbox\n            checked={false}\n            tabIndex={-1}\n            aria-hidden\n            className=\"pointer-events-none group-data-[selected]:invisible\"\n          />\n          <ComboboxPrimitive.ItemIndicator className=\"absolute inset-0 inline-flex items-center justify-center\">\n            <Checkbox\n              checked\n              tabIndex={-1}\n              aria-hidden\n              className=\"pointer-events-none\"\n            />\n          </ComboboxPrimitive.ItemIndicator>\n        </span>\n      )}\n      {children}\n      {onRemove && (\n        <span\n          role=\"button\"\n          aria-label={removeLabel}\n          tabIndex={-1}\n          onPointerDown={(e) => e.stopPropagation()}\n          onClick={(e) => {\n            e.stopPropagation();\n            e.preventDefault();\n            onRemove();\n          }}\n          className=\"ms-auto inline-flex size-5 shrink-0 items-center justify-center rounded-[var(--radius-8)] text-ink-muted transition-colors duration-[var(--duration-state)] ease-[var(--ease-standard)] hover:bg-layer-hover hover:text-ink active:bg-layer-hover\"\n          data-slot=\"combobox-item-remove\"\n        >\n          <X className=\"size-3\" aria-hidden />\n        </span>\n      )}\n    </ComboboxPrimitive.Item>\n  );\n}\n\n/* -------------------------------- Divider ----------------------------- */\n\nexport function ComboboxDivider({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>): React.ReactElement {\n  return (\n    <ComboboxPrimitive.Separator\n      data-slot=\"combobox-divider\"\n      className={cn(popupDivider, className)}\n      {...props}\n    />\n  );\n}\n\n/* ---------------------------- Group / Section ------------------------- */\n\nexport function ComboboxGroupLabel({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>): React.ReactElement {\n  return (\n    <ComboboxPrimitive.GroupLabel\n      data-slot=\"combobox-group-label\"\n      className={cn(\n        itemGroupLabel.base,\n        itemGroupLabel.comfortable,\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport interface ComboboxSectionProps\n  extends Omit<React.HTMLAttributes<HTMLDivElement>, \"children\" | \"title\"> {\n  title?: React.ReactNode;\n  children: React.ReactNode;\n}\n\nexport function ComboboxSection({\n  title,\n  className,\n  children,\n  ...props\n}: ComboboxSectionProps): React.ReactElement {\n  return (\n    <ComboboxPrimitive.Group\n      data-slot=\"combobox-section\"\n      className={cn(\"py-1 first:pt-0 last:pb-0\", className)}\n      {...props}\n    >\n      {title != null && <ComboboxGroupLabel>{title}</ComboboxGroupLabel>}\n      {children}\n    </ComboboxPrimitive.Group>\n  );\n}\n\nexport const ComboboxSearchIcon = MagnifyingGlass;\nconst ComboboxPrimitiveExport = ComboboxPrimitive;\nexport { ComboboxPrimitiveExport as ComboboxPrimitive };\n",
      "type": "registry:ui",
      "target": "components/ui/combobox.tsx"
    }
  ],
  "type": "registry:ui"
}