{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "combobox",
  "title": "Combobox",
  "description": "Base UI combobox — a text input filtered against a list as you type.",
  "dependencies": [
    "@base-ui/react",
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "springs",
    "use-proximity-hover"
  ],
  "files": [
    {
      "path": "src/components/ui/combobox.tsx",
      "content": "\"use client\";\n\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n  type ReactNode,\n} from \"react\";\nimport { Combobox as ComboboxPrimitive } from \"@base-ui/react/combobox\";\nimport { motion, AnimatePresence, useReducedMotion } from \"motion/react\";\nimport { CheckIcon, ChevronDownIcon, SearchIcon, XIcon } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { spring } from \"@/lib/springs\";\nimport {\n  useProximityHover,\n  proximityHoverWashClassName,\n  proximityHoverWashOpacity,\n} from \"@/hooks/use-proximity-hover\";\n\ninterface ComboboxProximityContextValue {\n  registerItem: (index: number, element: HTMLElement | null) => void;\n}\n\nconst ComboboxProximityContext = createContext<ComboboxProximityContextValue | null>(null);\n\nfunction Combobox<Value, Multiple extends boolean | undefined = false>({\n  ...props\n}: ComboboxPrimitive.Root.Props<Value, Multiple>) {\n  return <ComboboxPrimitive.Root data-slot=\"combobox\" {...props} />;\n}\n\nfunction ComboboxInputGroup({ className, ...props }: ComboboxPrimitive.InputGroup.Props) {\n  return (\n    <ComboboxPrimitive.InputGroup\n      data-slot=\"combobox-input-group\"\n      className={cn(\n        \"flex h-8 w-full min-w-0 items-center gap-1.5 rounded-lg border border-input bg-transparent pr-1.5 pl-2.5 transition-colors outline-none focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/50 has-data-disabled:pointer-events-none has-data-disabled:opacity-50 dark:bg-input/30\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction ComboboxInput({ className, ...props }: ComboboxPrimitive.Input.Props) {\n  return (\n    <ComboboxPrimitive.Input\n      data-slot=\"combobox-input\"\n      className={cn(\n        \"h-full min-w-0 flex-1 bg-transparent text-body text-foreground outline-none placeholder:text-muted-foreground\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction ComboboxSearchIcon({ className, ...props }: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"combobox-search-icon\"\n      aria-hidden\n      className={cn(\"flex shrink-0 items-center justify-center text-muted-foreground\", className)}\n      {...props}\n    >\n      <SearchIcon className=\"size-4\" />\n    </span>\n  );\n}\n\nfunction ComboboxIcon({ className, ...props }: ComboboxPrimitive.Icon.Props) {\n  return (\n    <ComboboxPrimitive.Icon\n      data-slot=\"combobox-icon\"\n      className={cn(\"shrink-0 text-muted-foreground\", className)}\n      {...props}\n    >\n      <ChevronDownIcon className=\"size-4\" />\n    </ComboboxPrimitive.Icon>\n  );\n}\n\nfunction ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {\n  return (\n    <ComboboxPrimitive.Clear\n      data-slot=\"combobox-clear\"\n      className={cn(\n        \"flex shrink-0 items-center justify-center rounded-md p-0.5 text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring/50\",\n        className,\n      )}\n      {...props}\n    >\n      <XIcon className=\"size-3.5\" />\n    </ComboboxPrimitive.Clear>\n  );\n}\n\n/**\n * Matches the trigger's width (`w-(--anchor-width)`) rather than sizing to\n * content the way MenuContent does — a combobox popup is \"results for what's\n * typed in this exact field,\" so lining its edges up with the input is the\n * legible choice, unlike a dropdown menu's independent action list. Same\n * `--popover` elevation step and `spring.moderate` scale-in as Popover/Menu\n * otherwise. Its result surface follows filtering with the same shared\n * spring, maintaining a continuous height transition as rows are added or\n * removed.\n */\nfunction ComboboxPopup({ className, children, ...props }: ComboboxPrimitive.Popup.Props) {\n  // Measure the natural result-set height and interpolate between each value\n  // as filtering adds or removes rows. The outer shell continues to cap and\n  // scroll long result sets.\n  const roRef = useRef<ResizeObserver | null>(null);\n  const [contentHeight, setContentHeight] = useState<number | null>(null);\n\n  const measureRef = useCallback((el: HTMLDivElement | null) => {\n    roRef.current?.disconnect();\n    roRef.current = null;\n    if (!el) return;\n    const measure = () => {\n      if (el.offsetHeight > 0) {\n        setContentHeight(el.offsetHeight);\n      }\n    };\n    measure();\n    const ro = new ResizeObserver(measure);\n    ro.observe(el);\n    roRef.current = ro;\n  }, []);\n\n  return (\n    <ComboboxPrimitive.Popup\n      data-slot=\"combobox-content\"\n      render={(popupProps, state) => {\n        const exiting = state.transitionStatus === \"ending\";\n        return (\n          <motion.div\n            {...(popupProps as Record<string, unknown>)}\n            {...(props as Record<string, unknown>)}\n            className={cn(\n              \"z-50 max-h-(--available-height) w-(--anchor-width) origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-popover outline-none\",\n              className,\n            )}\n            initial={{ opacity: 0, scale: 0.98 }}\n            animate={{ opacity: exiting ? 0 : 1, scale: exiting ? 0.98 : 1 }}\n            transition={exiting ? spring.moderate.exit : spring.moderate.enter}\n          >\n            <motion.div\n              initial={false}\n              animate={{ height: contentHeight ?? \"auto\" }}\n              transition={spring.moderate.enter}\n              className=\"overflow-hidden\"\n            >\n              <div ref={measureRef}>{children}</div>\n            </motion.div>\n          </motion.div>\n        );\n      }}\n    />\n  );\n}\n\nfunction ComboboxContent({\n  align = \"start\",\n  alignOffset = 0,\n  side = \"bottom\",\n  sideOffset = 6,\n  className,\n  ...props\n}: ComboboxPrimitive.Popup.Props &\n  Pick<ComboboxPrimitive.Positioner.Props, \"align\" | \"alignOffset\" | \"side\" | \"sideOffset\">) {\n  return (\n    <ComboboxPrimitive.Portal>\n      <ComboboxPrimitive.Positioner\n        data-slot=\"combobox-positioner\"\n        align={align}\n        alignOffset={alignOffset}\n        side={side}\n        sideOffset={sideOffset}\n        className=\"z-50 outline-none\"\n      >\n        <ComboboxPopup className={className} {...props} />\n      </ComboboxPrimitive.Positioner>\n    </ComboboxPrimitive.Portal>\n  );\n}\n\nfunction ComboboxList({ className, children, ...props }: ComboboxPrimitive.List.Props) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const { activeIndex, itemRects, handlers, registerItem, measureItems } = useProximityHover(\n    containerRef,\n    { axis: \"y\" },\n  );\n\n  useEffect(() => {\n    measureItems();\n  }, [measureItems, children]);\n\n  const activeRect = activeIndex !== null ? itemRects[activeIndex] : null;\n  // Without this, `{ registerItem }` is a fresh object every render, so\n  // every item's registration effect (keyed on this context value) re-fires\n  // every render, bumps useProximityHover's registerTick, and re-renders\n  // this list — an infinite loop caught as \"Maximum update depth\n  // exceeded.\" `registerItem` itself is already a stable useCallback.\n  const proximityContextValue = useMemo(() => ({ registerItem }), [registerItem]);\n\n  return (\n    <ComboboxPrimitive.List\n      ref={containerRef}\n      data-slot=\"combobox-list\"\n      render={(listProps) => (\n        <div\n          {...(listProps as Record<string, unknown>)}\n          {...(props as Record<string, unknown>)}\n          onMouseMove={handlers.onMouseMove}\n          onMouseEnter={handlers.onMouseEnter}\n          onMouseLeave={handlers.onMouseLeave}\n          className={cn(\"relative flex flex-col gap-0.5\", className)}\n        >\n          <AnimatePresence>\n            {activeRect && (\n              <motion.div\n                className={cn(\n                  \"pointer-events-none absolute rounded-md\",\n                  proximityHoverWashClassName,\n                )}\n                initial={{\n                  opacity: 0,\n                  top: activeRect.top,\n                  left: activeRect.left,\n                  width: activeRect.width,\n                  height: activeRect.height,\n                }}\n                animate={{\n                  opacity: proximityHoverWashOpacity,\n                  top: activeRect.top,\n                  left: activeRect.left,\n                  width: activeRect.width,\n                  height: activeRect.height,\n                }}\n                exit={{ opacity: 0, transition: spring.fast.exit }}\n                transition={spring.fast.enter}\n              />\n            )}\n          </AnimatePresence>\n          <ComboboxProximityContext.Provider value={proximityContextValue}>\n            {(listProps as { children?: ReactNode }).children}\n          </ComboboxProximityContext.Provider>\n        </div>\n      )}\n    >\n      {children}\n    </ComboboxPrimitive.List>\n  );\n}\n\nfunction ComboboxGroup({ ...props }: ComboboxPrimitive.Group.Props) {\n  return <ComboboxPrimitive.Group data-slot=\"combobox-group\" {...props} />;\n}\n\nfunction ComboboxGroupLabel({ className, ...props }: ComboboxPrimitive.GroupLabel.Props) {\n  return (\n    <ComboboxPrimitive.GroupLabel\n      data-slot=\"combobox-group-label\"\n      className={cn(\"px-2 py-1.5 text-label text-muted-foreground uppercase\", className)}\n      {...props}\n    />\n  );\n}\n\n/**\n * Position for proximity hover. ComboboxList's `children` is a Base UI\n * render-prop (`(item, index) => ReactNode`) it calls once per filtered\n * item, so the index proximity hover needs is already sitting right there\n * at each call site — pass it straight through rather than re-deriving it.\n */\nfunction useComboboxItemRegistration(ref: React.RefObject<HTMLElement | null>, index?: number) {\n  const ctx = useContext(ComboboxProximityContext);\n  useEffect(() => {\n    if (index === undefined || !ctx) return;\n    ctx.registerItem(index, ref.current);\n    return () => ctx.registerItem(index, null);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [index, ctx]);\n}\n\nfunction ComboboxItem({\n  className,\n  children,\n  index,\n  render,\n  ...props\n}: ComboboxPrimitive.Item.Props) {\n  const ref = useRef<HTMLDivElement>(null);\n  const reduceMotion = useReducedMotion();\n  useComboboxItemRegistration(ref, index);\n\n  return (\n    <ComboboxPrimitive.Item\n      ref={ref}\n      index={index}\n      data-slot=\"combobox-item\"\n      className={cn(\n        // Persistent selected-item tint, not the transient proximity-hover\n        // wash below (--hover). bg-accent would read almost identically to\n        // --popover here (--accent is neutral gray, only ~0.03 L off\n        // --popover in dark mode) — --active is the same foreground-tint\n        // mechanism as the hover wash, at a constant, clearly stronger\n        // opacity instead of the wash's capped/animated peak, so \"selected\"\n        // reads heavier than a passing hover rather than a coincidentally\n        // similar shade. See --active's definition in globals.css.\n        \"relative z-10 flex cursor-default items-center gap-2 rounded-md py-1.5 pr-8 pl-2 text-control text-muted-foreground outline-none transition-colors select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-highlighted:text-foreground data-[selected]:bg-active data-[selected]:text-foreground\",\n        className,\n      )}\n      {...props}\n      render={\n        render ??\n        ((itemProps) => (\n          <motion.div\n            {...(itemProps as React.ComponentProps<typeof motion.div>)}\n            initial={{ opacity: 0, y: reduceMotion ? 0 : 3 }}\n            animate={{ opacity: 1, y: 0 }}\n            transition={spring.quick.enter}\n          />\n        ))\n      }\n    >\n      {children}\n      <span\n        className=\"pointer-events-none absolute right-2 flex items-center justify-center\"\n        data-slot=\"combobox-item-indicator\"\n      >\n        {/* Selection indicators are `spring.fast` — same tier and pattern as\n            MenuCheckboxItem's check mark. `keepMounted` lets framer play the\n            pop-out when a different item is selected instead of Base UI\n            unmounting it first. */}\n        <ComboboxPrimitive.ItemIndicator\n          keepMounted\n          render={(indicatorProps, state) => {\n            const visible = state.selected && state.transitionStatus !== \"ending\";\n            return (\n              <motion.span\n                {...(indicatorProps as Record<string, unknown>)}\n                initial={false}\n                animate={{ opacity: visible ? 1 : 0, scale: visible ? 1 : 0.5 }}\n                transition={visible ? spring.fast.enter : spring.fast.exit}\n              >\n                <CheckIcon className=\"size-3.5\" />\n              </motion.span>\n            );\n          }}\n        />\n      </span>\n    </ComboboxPrimitive.Item>\n  );\n}\n\n/**\n * Base UI keeps this element mounted at all times, even with a non-empty\n * list, so screen readers reliably pick up its `aria-live` announcements —\n * it only conditionally renders its *children* (see ComboboxEmpty.js). With\n * results present it's a childless div, so the padding below must collapse\n * via `empty:` (`:empty` matches — no children were rendered) instead of\n * applying unconditionally, or a dead gap sits above the list.\n */\nfunction ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {\n  return (\n    <ComboboxPrimitive.Empty\n      data-slot=\"combobox-empty\"\n      className={cn(\n        \"empty:p-0 px-2 py-6 text-center text-caption text-muted-foreground\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction ComboboxTrigger({ className, ...props }: ComboboxPrimitive.Trigger.Props) {\n  return (\n    <ComboboxPrimitive.Trigger\n      data-slot=\"combobox-trigger\"\n      className={cn(\"flex shrink-0 items-center justify-center\", className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Combobox,\n  ComboboxInputGroup,\n  ComboboxInput,\n  ComboboxIcon,\n  ComboboxSearchIcon,\n  ComboboxClear,\n  ComboboxTrigger,\n  ComboboxContent,\n  ComboboxList,\n  ComboboxGroup,\n  ComboboxGroupLabel,\n  ComboboxItem,\n  ComboboxEmpty,\n};\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}