{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "combobox",
  "title": "Combobox",
  "description": "A filterable select with live search, proximity highlight, and native select mirror for forms.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "components/ui/combobox.tsx",
      "type": "registry:ui",
      "content": "'use client'\n\nimport {\n  createContext,\n  forwardRef,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n  type HTMLAttributes,\n  type ReactNode,\n} from 'react'\nimport { createPortal } from 'react-dom'\nimport { type MotionValue } from 'motion/react'\nimport { cn } from '@/lib/utils'\nimport { useProximityHighlight, ProximityHighlight } from '@/lib/hooks/use-proximity-highlight'\nimport { matchesSearch, buildSearchableText } from '@/lib/search-match'\n\nexport interface ComboboxOption {\n  value: string\n  label: string\n  description?: string\n}\n\ninterface ComboboxContextValue {\n  value: string\n  setValue: (v: string) => void\n  open: boolean\n  setOpen: (next: boolean) => void\n  query: string\n  setQuery: (q: string) => void\n  disabled: boolean\n  triggerId: string\n  contentId: string\n  nextIndex: () => number\n  labelMap: React.MutableRefObject<Map<string, string>>\n  registerOption: (value: string, label: string) => void\n  options: ComboboxOption[]\n  placeholder: string\n  emptyMessage: string\n  clearable: boolean\n  openOnFocus: boolean\n  setValueRef: React.MutableRefObject<(v: string) => void>\n  activeDescendantId: string | null\n  setActiveDescendantId: (id: string | null) => void\n  suppressNextFocusOpenRef: React.MutableRefObject<boolean>\n}\n\nconst ComboboxContext = createContext<ComboboxContextValue | null>(null)\n\nfunction useComboboxCtx(componentName: string) {\n  const ctx = useContext(ComboboxContext)\n  if (!ctx) throw new Error(`${componentName} must be used within Combobox`)\n  return ctx\n}\n\ninterface ComboboxContentContextValue {\n  activeIndex: number | null\n  setActiveIndex: (i: number | null) => void\n  highlightX: ReturnType<typeof import('motion/react').useSpring>\n  highlightSize: MotionValue<number>\n  highlightOpacity: ReturnType<typeof import('motion/react').useSpring>\n  axis: 'x' | 'y'\n  registerItem: (index: number, el: HTMLElement | null) => void\n  filteredOptions: ComboboxOption[]\n  setValueRef: React.MutableRefObject<(v: string) => void>\n}\n\nconst ComboboxContentContext = createContext<ComboboxContentContextValue | null>(null)\n\nfunction useComboboxContentCtx(componentName: string) {\n  const ctx = useContext(ComboboxContentContext)\n  if (!ctx) throw new Error(`${componentName} must be used within ComboboxContent`)\n  return ctx\n}\n\nexport interface ComboboxProps {\n  children: ReactNode\n  options: ComboboxOption[]\n  value?: string\n  defaultValue?: string\n  onValueChange?: (value: string) => void\n  placeholder?: string\n  emptyMessage?: string\n  disabled?: boolean\n  clearable?: boolean\n  openOnFocus?: boolean\n  name?: string\n  required?: boolean\n  filter?: (option: ComboboxOption, query: string) => boolean\n  className?: string\n}\n\nexport const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(\n  (\n    {\n      children,\n      options,\n      value: valueProp,\n      defaultValue = '',\n      onValueChange,\n      placeholder = 'Search…',\n      emptyMessage = 'No results.',\n      disabled = false,\n      clearable = true,\n      openOnFocus = false,\n      name,\n      required = false,\n      className,\n    },\n    ref,\n  ) => {\n    const isControlled = valueProp !== undefined\n    const [internalValue, setInternalValue] = useState(defaultValue)\n    const value = isControlled ? valueProp : internalValue\n\n    const [open, setOpen] = useState(false)\n    const [query, setQuery] = useState('')\n    const [activeDescendantId, setActiveDescendantId] = useState<string | null>(null)\n    const labelMap = useRef(new Map<string, string>())\n    const selectRef = useRef<HTMLSelectElement>(null)\n    const suppressNextFocusOpenRef = useRef(false)\n\n    const reactId = useId()\n    const triggerId = `${reactId}-trigger`\n    const contentId = `${reactId}-content`\n\n    const [registeredOptions, setRegisteredOptions] = useState<{ value: string; label: string }[]>(\n      [],\n    )\n    const registerOption = useCallback((v: string, label: string) => {\n      labelMap.current.set(v, label)\n      setRegisteredOptions((prev) => {\n        const idx = prev.findIndex((o) => o.value === v)\n        if (idx >= 0) {\n          if (prev[idx].label === label) return prev\n          const next = [...prev]\n          next[idx] = { value: v, label }\n          return next\n        }\n        return [...prev, { value: v, label }]\n      })\n    }, [])\n\n    const setValue = useCallback(\n      (v: string) => {\n        if (!isControlled) setInternalValue(v)\n        onValueChange?.(v)\n        setQuery('')\n        setOpen(false)\n        suppressNextFocusOpenRef.current = true\n        document.getElementById(triggerId)?.focus()\n      },\n      [isControlled, onValueChange, triggerId],\n    )\n\n    const setValueRef = useRef(setValue)\n    useEffect(() => {\n      setValueRef.current = setValue\n    })\n\n    useEffect(() => {\n      options.forEach((opt) => registerOption(opt.value, opt.label))\n    }, [options, registerOption])\n\n    useEffect(() => {\n      if (!open) return\n      const handlePointerDown = (e: PointerEvent) => {\n        const target = e.target as Node\n        if ((target as HTMLElement).closest?.('[data-combobox-trigger]')) return\n        if ((target as HTMLElement).closest?.('[data-combobox-content]')) return\n        setOpen(false)\n      }\n      document.addEventListener('pointerdown', handlePointerDown)\n      return () => document.removeEventListener('pointerdown', handlePointerDown)\n    }, [open])\n\n    useEffect(() => {\n      if (!open) return\n      const handleKeyDown = (e: KeyboardEvent) => {\n        if (e.key === 'Escape') {\n          e.preventDefault()\n          setOpen(false)\n          document.getElementById(triggerId)?.focus()\n        }\n      }\n      document.addEventListener('keydown', handleKeyDown)\n      return () => document.removeEventListener('keydown', handleKeyDown)\n    }, [open, triggerId])\n\n    const ctx = useMemo<ComboboxContextValue>(\n      () => ({\n        value,\n        setValue,\n        open,\n        setOpen,\n        query,\n        setQuery,\n        disabled,\n        triggerId,\n        contentId,\n        nextIndex: () => 0,\n        labelMap,\n        registerOption,\n        options,\n        placeholder,\n        emptyMessage,\n        clearable,\n        openOnFocus,\n        setValueRef,\n        activeDescendantId,\n        setActiveDescendantId,\n        suppressNextFocusOpenRef,\n      }),\n      [\n        value,\n        setValue,\n        open,\n        query,\n        disabled,\n        triggerId,\n        contentId,\n        registerOption,\n        options,\n        placeholder,\n        emptyMessage,\n        clearable,\n        openOnFocus,\n        activeDescendantId,\n      ],\n    )\n\n    const indexRef = useRef(0)\n    const nextIndex = useCallback(() => {\n      const idx = indexRef.current\n      indexRef.current += 1\n      return idx\n    }, [])\n    useEffect(() => {\n      if (!open) indexRef.current = 0\n    }, [open])\n\n    const fullCtx = useMemo<ComboboxContextValue>(() => ({ ...ctx, nextIndex }), [ctx, nextIndex])\n\n    return (\n      <ComboboxContext.Provider value={fullCtx}>\n        <div ref={ref} className={cn('relative inline-block', className)}>\n          {children}\n          <select\n            ref={selectRef}\n            tabIndex={-1}\n            aria-hidden=\"true\"\n            className=\"sr-only\"\n            name={name}\n            required={required}\n            value={value}\n            disabled={disabled}\n            onChange={(e) => setValue(e.target.value)}\n            onFocus={(e) => e.preventDefault()}\n          >\n            <option value=\"\"></option>\n            {registeredOptions.map((opt) => (\n              <option key={opt.value} value={opt.value}>\n                {opt.label}\n              </option>\n            ))}\n          </select>\n        </div>\n      </ComboboxContext.Provider>\n    )\n  },\n)\nCombobox.displayName = 'Combobox'\n\nexport interface ComboboxInputProps extends Omit<HTMLAttributes<HTMLInputElement>, 'children'> {\n  placeholder?: string\n}\n\nexport const ComboboxInput = forwardRef<HTMLInputElement, ComboboxInputProps>(\n  ({ placeholder: placeholderProp, className, onFocus, ...props }, ref) => {\n    const {\n      value,\n      setValue,\n      open,\n      setOpen,\n      query,\n      setQuery,\n      disabled,\n      triggerId,\n      contentId,\n      labelMap,\n      placeholder: ctxPlaceholder,\n      clearable,\n      activeDescendantId,\n      suppressNextFocusOpenRef,\n    } = useComboboxCtx('ComboboxInput')\n    const placeholder = placeholderProp ?? ctxPlaceholder\n    const displayLabel = value ? (labelMap.current.get(value) ?? value) : undefined\n\n    const inputRef = useRef<HTMLInputElement>(null)\n\n    const setRefs = useCallback(\n      (node: HTMLInputElement | null) => {\n        ;(inputRef as React.MutableRefObject<HTMLInputElement | null>).current = node\n        if (typeof ref === 'function') ref(node)\n        else if (ref) (ref as React.MutableRefObject<HTMLInputElement | null>).current = node\n      },\n      [ref],\n    )\n\n    return (\n      <div className=\"relative flex min-h-11 w-full items-center\">\n        <input\n          ref={setRefs}\n          id={triggerId}\n          type=\"text\"\n          role=\"combobox\"\n          aria-expanded={open}\n          aria-haspopup=\"listbox\"\n          aria-controls={open ? contentId : undefined}\n          aria-autocomplete=\"list\"\n          aria-activedescendant={open ? (activeDescendantId ?? undefined) : undefined}\n          disabled={disabled}\n          data-combobox-trigger\n          value={query || (open ? '' : (displayLabel ?? ''))}\n          placeholder={open ? placeholder : displayLabel ? '' : placeholder}\n          className={cn(\n            'h-11 w-full rounded-lg squircle-corners border border-(--color-border) bg-(--color-surface) pe-10 ps-4 py-2.5 text-sm text-(--color-fg) outline-none transition-colors duration-(--motion-dur-fast) motion-reduce:transition-none',\n            'placeholder:text-(--color-muted)',\n            'focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-2 focus-visible:ring-offset-(--color-bg)',\n            'disabled:cursor-not-allowed disabled:opacity-50',\n            className,\n          )}\n          onFocus={(e) => {\n            onFocus?.(e)\n            if (suppressNextFocusOpenRef.current) {\n              suppressNextFocusOpenRef.current = false\n              return\n            }\n            setOpen(true)\n          }}\n          onChange={(e) => {\n            setQuery(e.target.value)\n            if (!open) setOpen(true)\n          }}\n          onKeyDown={(e) => {\n            if (e.key === 'Enter') {\n              e.preventDefault()\n            }\n          }}\n          {...props}\n        />\n        {clearable && value && (\n          <button\n            type=\"button\"\n            aria-label=\"Clear selection\"\n            className=\"absolute inset-e-2 top-1/2 flex size-7 -translate-y-1/2 items-center justify-center rounded-full text-(--color-muted) transition-colors duration-(--motion-dur-fast) motion-reduce:transition-none hover:text-(--color-fg)\"\n            onClick={() => {\n              setValue('')\n              setQuery('')\n              inputRef.current?.focus()\n            }}\n          >\n            <svg\n              width=\"14\"\n              height=\"14\"\n              viewBox=\"0 0 16 16\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              strokeWidth=\"1.5\"\n              strokeLinecap=\"round\"\n            >\n              <path d=\"M4 4l8 8M12 4l-8 8\" />\n            </svg>\n          </button>\n        )}\n        <span\n          className=\"absolute inset-e-10 top-1/2 -translate-y-1/2 text-(--color-muted)\"\n          aria-hidden=\"true\"\n          style={{\n            display: 'none',\n          }}\n        />\n      </div>\n    )\n  },\n)\nComboboxInput.displayName = 'ComboboxInput'\n\nexport interface ComboboxContentProps extends HTMLAttributes<HTMLDivElement> {\n  align?: 'start' | 'center' | 'end'\n  side?: 'bottom' | 'top'\n  sideOffset?: number\n}\n\nexport const ComboboxContent = forwardRef<HTMLDivElement, ComboboxContentProps>(\n  ({ align = 'start', side = 'bottom', sideOffset = 8, className, ...props }, ref) => {\n    const {\n      open,\n      setOpen,\n      triggerId,\n      contentId,\n      query,\n      options: allOptions,\n      setValueRef,\n      setActiveDescendantId,\n    } = useComboboxCtx('ComboboxContent')\n    const panelRef = useRef<HTMLDivElement | null>(null)\n    const [position, setPosition] = useState({ left: 0, top: 0 })\n    const [positioned, setPositioned] = useState(false)\n\n    const filteredOptions = useMemo(() => {\n      if (!query) return allOptions\n      return allOptions.filter((opt) =>\n        matchesSearch(buildSearchableText(opt.label, opt.description), query),\n      )\n    }, [allOptions, query])\n\n    const {\n      activeIndex,\n      setActiveIndex,\n      registerItem,\n      handlers,\n      highlightX,\n      highlightSize,\n      highlightOpacity,\n      axis,\n    } = useProximityHighlight(panelRef, { axis: 'y' })\n\n    const setActiveIndexWithDescendant = useCallback(\n      (index: number | null) => {\n        setActiveIndex(index)\n        const option = index !== null ? filteredOptions[index] : undefined\n        setActiveDescendantId(option ? `combobox-option-${option.value}` : null)\n      },\n      [setActiveIndex, setActiveDescendantId, filteredOptions],\n    )\n\n    useEffect(() => {\n      if (!open) return\n      const measure = () => {\n        const trigger = document.getElementById(triggerId)\n        const content = panelRef.current\n        if (!trigger || !content) return\n        const triggerRect = trigger.getBoundingClientRect()\n        const contentRect = content.getBoundingClientRect()\n        const vw = window.innerWidth\n        const vh = window.innerHeight\n\n        let left = align === 'end' ? triggerRect.right - contentRect.width : triggerRect.left\n        if (align === 'center')\n          left = triggerRect.left + (triggerRect.width - contentRect.width) / 2\n        left = Math.max(12, Math.min(left, vw - contentRect.width - 12))\n\n        let top: number\n        const spaceBelow = vh - triggerRect.bottom - sideOffset\n        if (side === 'top' || (side === 'bottom' && spaceBelow < contentRect.height)) {\n          top = triggerRect.top - contentRect.height - sideOffset\n        } else {\n          top = triggerRect.bottom + sideOffset\n        }\n        setPosition({ left, top })\n        setPositioned(true)\n      }\n\n      const frame = requestAnimationFrame(measure)\n      window.addEventListener('resize', measure)\n      window.addEventListener('scroll', measure, true)\n      return () => {\n        cancelAnimationFrame(frame)\n        window.removeEventListener('resize', measure)\n        window.removeEventListener('scroll', measure, true)\n      }\n    }, [open, align, side, sideOffset, triggerId])\n\n    useEffect(() => {\n      if (!open) return\n      const handleKeyDown = (e: KeyboardEvent) => {\n        const target = e.target as HTMLElement\n        if (!target?.getAttribute('data-combobox-trigger')) return\n\n        const count = filteredOptions.length\n        if (!count) return\n\n        if (e.key === 'ArrowDown') {\n          e.preventDefault()\n          const next = activeIndex === null ? 0 : (activeIndex + 1) % count\n          setActiveIndexWithDescendant(next)\n        } else if (e.key === 'ArrowUp') {\n          e.preventDefault()\n          const next = activeIndex === null ? count - 1 : (activeIndex - 1 + count) % count\n          setActiveIndexWithDescendant(next)\n        } else if (e.key === 'Home') {\n          e.preventDefault()\n          setActiveIndexWithDescendant(0)\n        } else if (e.key === 'End') {\n          e.preventDefault()\n          setActiveIndexWithDescendant(count - 1)\n        } else if (e.key === 'Enter') {\n          e.preventDefault()\n          if (activeIndex !== null && filteredOptions[activeIndex]) {\n            setValueRef.current(filteredOptions[activeIndex].value)\n          }\n        } else if (e.key === 'Tab') {\n          setOpen(false)\n        }\n      }\n      document.addEventListener('keydown', handleKeyDown)\n      return () => document.removeEventListener('keydown', handleKeyDown)\n    }, [\n      open,\n      activeIndex,\n      filteredOptions,\n      setActiveIndexWithDescendant,\n      triggerId,\n      setOpen,\n      setValueRef,\n    ])\n\n    const setRefs = useCallback(\n      (el: HTMLDivElement | null) => {\n        ;(panelRef as React.MutableRefObject<HTMLDivElement | null>).current = el\n        if (typeof ref === 'function') ref(el)\n        else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = el\n      },\n      [ref],\n    )\n\n    const contentCtx = useMemo<ComboboxContentContextValue>(\n      () => ({\n        activeIndex,\n        setActiveIndex: setActiveIndexWithDescendant,\n        highlightX,\n        highlightSize,\n        highlightOpacity,\n        axis,\n        registerItem,\n        filteredOptions,\n        setValueRef,\n      }),\n      [\n        activeIndex,\n        setActiveIndexWithDescendant,\n        highlightX,\n        highlightSize,\n        highlightOpacity,\n        axis,\n        registerItem,\n        filteredOptions,\n        setValueRef,\n      ],\n    )\n\n    if (!open) return null\n\n    const content = (\n      <ComboboxContentContext.Provider value={contentCtx}>\n        <div\n          ref={setRefs}\n          id={contentId}\n          role=\"listbox\"\n          aria-labelledby={triggerId}\n          data-combobox-content\n          className={cn(\n            'relative z-30 min-w-48 max-w-[calc(100vw-1.5rem)] overflow-hidden rounded-lg squircle-corners border border-(--color-border) bg-(--color-bg) p-1.5 outline-none',\n            'shadow-[0_14px_34px_-22px_rgba(0,0,0,0.15)]',\n            className,\n          )}\n          style={{\n            position: 'fixed',\n            left: position.left,\n            top: position.top,\n            maxHeight: 320,\n            overflowY: 'auto',\n            visibility: positioned ? 'visible' : 'hidden',\n          }}\n          tabIndex={-1}\n          {...handlers}\n          {...props}\n        >\n          <ProximityHighlight\n            highlightX={highlightX}\n            highlightSize={highlightSize}\n            highlightOpacity={highlightOpacity}\n            axis={axis}\n            className=\"mx-1.5 rounded-md squircle-corners bg-(--color-surface-2)\"\n          />\n          {filteredOptions.length > 0 ? (\n            filteredOptions.map((opt, i) => (\n              <ComboboxOptionItem key={opt.value} option={opt} index={i} />\n            ))\n          ) : (\n            <div role=\"status\" className=\"px-3 py-2.5 text-sm text-(--color-muted)\">\n              {query ? 'No results.' : 'No options.'}\n            </div>\n          )}\n        </div>\n      </ComboboxContentContext.Provider>\n    )\n\n    return createPortal(content, document.body)\n  },\n)\nComboboxContent.displayName = 'ComboboxContent'\n\nfunction ComboboxOptionItem({ option, index }: { option: ComboboxOption; index: number }) {\n  const { setValue } = useComboboxCtx('ComboboxOptionItem')\n  const { activeIndex, setActiveIndex, registerItem } = useComboboxContentCtx('ComboboxOptionItem')\n  const itemRef = useRef<HTMLDivElement | null>(null)\n  const isActive = activeIndex === index\n\n  useEffect(() => {\n    const el = itemRef.current\n    if (index < 0 || !el) return\n    registerItem(index, el)\n    return () => registerItem(index, null)\n  }, [index, registerItem])\n\n  return (\n    <div\n      ref={itemRef}\n      role=\"option\"\n      id={`combobox-option-${option.value}`}\n      aria-selected={isActive}\n      data-value={option.value}\n      data-proximity-index={index}\n      className={cn(\n        'relative flex min-h-11 w-full cursor-default select-none scroll-m-1 items-center gap-3 rounded-md squircle-corners px-3 py-2.5 text-left text-sm outline-none transition-colors duration-(--motion-dur-fast) motion-reduce:transition-none',\n        isActive ? 'text-(--color-fg)' : 'text-(--color-muted)',\n      )}\n      onMouseEnter={() => setActiveIndex(index)}\n      onClick={() => setValue(option.value)}\n    >\n      <span className=\"relative z-10 flex min-w-0 flex-1 flex-col gap-0.5\">\n        <span className=\"truncate\">{option.label}</span>\n        {option.description && (\n          <span className=\"text-xs text-(--color-subtle) truncate\">{option.description}</span>\n        )}\n      </span>\n    </div>\n  )\n}\n\nexport function ComboboxPreview() {\n  const [value, setValue] = useState('')\n  return (\n    <div\n      className=\"flex h-full w-full items-center justify-center p-6\"\n      style={{ backgroundColor: 'var(--color-surface)' }}\n    >\n      <div className=\"w-64\">\n        <Combobox\n          value={value}\n          onValueChange={setValue}\n          options={[\n            { value: 'ist', label: 'Istanbul', description: 'Türkiye' },\n            { value: 'ber', label: 'Berlin' },\n            { value: 'par', label: 'Paris', description: 'France' },\n            { value: 'tok', label: 'Tokyo', description: 'Japan' },\n          ]}\n          placeholder=\"Search city…\"\n          name=\"city\"\n        >\n          <ComboboxInput />\n          <ComboboxContent />\n        </Combobox>\n      </div>\n    </div>\n  )\n}\n"
    },
    {
      "path": "lib/hooks/use-proximity-highlight.tsx",
      "type": "registry:lib",
      "content": "'use client'\n\nimport { useRef, useState, useCallback, useEffect, type RefObject } from 'react'\nimport {\n  motion,\n  useMotionValue,\n  useReducedMotion,\n  useSpring,\n  type MotionValue,\n  type MotionStyle,\n} from 'motion/react'\nimport { springs } from '@/lib/motion-tokens'\n\nexport interface ItemRect {\n  top: number\n  height: number\n  left: number\n  width: number\n}\n\ninterface UseProximityHighlightOptions {\n  axis?: 'x' | 'y'\n  spring?: { stiffness: number; damping: number; mass: number }\n}\n\ninterface UseProximityHighlightReturn {\n  activeIndex: number | null\n  setActiveIndex: (index: number | null) => void\n  itemRects: ItemRect[]\n  sessionRef: RefObject<number>\n  handlers: {\n    onMouseMove: (e: React.MouseEvent) => void\n    onMouseEnter: () => void\n    onMouseLeave: () => void\n  }\n  registerItem: (index: number, element: HTMLElement | null) => void\n  measureItems: () => void\n  highlightX: ReturnType<typeof useSpring>\n  highlightSize: MotionValue<number>\n  highlightOpacity: ReturnType<typeof useSpring>\n  axis: 'x' | 'y'\n}\n\nexport function useProximityHighlight<T extends HTMLElement>(\n  containerRef: RefObject<T | null>,\n  options: UseProximityHighlightOptions = {},\n): UseProximityHighlightReturn {\n  const { axis = 'y', spring: springConfig } = options\n  const reduceMotion = useReducedMotion()\n\n  const itemsRef = useRef(new Map<number, HTMLElement>())\n  const [activeIndex, setActiveIndexRaw] = useState<number | null>(null)\n  const activeIndexRef = useRef<number | null>(null)\n  const [itemRects, setItemRects] = useState<ItemRect[]>([])\n  const itemRectsRef = useRef<ItemRect[]>([])\n  const sessionRef = useRef(0)\n  const rafIdRef = useRef<number | null>(null)\n  const remeasureRafIdRef = useRef<number | null>(null)\n  const updateHighlightRef = useRef<(index: number | null) => void>(() => {})\n\n  const rawX = useMotionValue(0)\n  const highlightSize = useMotionValue(0)\n  const rawOpacity = useMotionValue(0)\n\n  const springOpts = reduceMotion ? { duration: 0 } : (springConfig ?? springs.settle)\n\n  const highlightX = useSpring(rawX, springOpts)\n  const highlightOpacity = useSpring(rawOpacity, springOpts)\n\n  const measureItems = useCallback(() => {\n    const container = containerRef.current\n    if (!container) return\n    const rects: ItemRect[] = []\n    itemsRef.current.forEach((element, index) => {\n      rects[index] = {\n        top: element.offsetTop,\n        height: element.offsetHeight,\n        left: element.offsetLeft,\n        width: element.offsetWidth,\n      }\n    })\n    itemRectsRef.current = rects\n    setItemRects(rects)\n  }, [containerRef])\n\n  const registerItem = useCallback(\n    (index: number, element: HTMLElement | null) => {\n      if (element) {\n        itemsRef.current.set(index, element)\n      } else {\n        itemsRef.current.delete(index)\n      }\n      if (remeasureRafIdRef.current !== null) {\n        cancelAnimationFrame(remeasureRafIdRef.current)\n      }\n      remeasureRafIdRef.current = requestAnimationFrame(() => {\n        remeasureRafIdRef.current = null\n        measureItems()\n      })\n    },\n    [measureItems],\n  )\n\n  const updateHighlight = useCallback(\n    (index: number | null) => {\n      if (index === null || !itemRectsRef.current[index]) {\n        if (reduceMotion) {\n          rawOpacity.jump(0)\n        } else {\n          rawOpacity.set(0)\n        }\n        return\n      }\n      const r = itemRectsRef.current[index]\n      if (axis === 'y') {\n        if (reduceMotion) {\n          rawX.jump(r.top)\n          highlightSize.jump(r.height)\n          rawOpacity.jump(1)\n        } else {\n          rawX.set(r.top)\n          highlightSize.jump(r.height)\n          rawOpacity.set(1)\n        }\n      } else {\n        if (reduceMotion) {\n          rawX.jump(r.left)\n          highlightSize.jump(r.width)\n          rawOpacity.jump(1)\n        } else {\n          rawX.set(r.left)\n          highlightSize.jump(r.width)\n          rawOpacity.set(1)\n        }\n      }\n    },\n    [axis, reduceMotion, rawX, highlightSize, rawOpacity],\n  )\n\n  useEffect(() => {\n    updateHighlightRef.current = updateHighlight\n  })\n\n  const setActiveIndex = useCallback((index: number | null) => {\n    setActiveIndexRaw(index)\n    updateHighlightRef.current(index)\n  }, [])\n\n  const handleMouseMove = useCallback(\n    (e: React.MouseEvent) => {\n      const mouseX = e.clientX\n      const mouseY = e.clientY\n\n      if (rafIdRef.current !== null) {\n        cancelAnimationFrame(rafIdRef.current)\n      }\n\n      rafIdRef.current = requestAnimationFrame(() => {\n        rafIdRef.current = null\n        const container = containerRef.current\n        if (!container) return\n\n        const containerRect = container.getBoundingClientRect()\n        const mousePos = axis === 'x' ? mouseX : mouseY\n\n        let closestIndex: number | null = null\n        let closestDistance = Infinity\n        let containingIndex: number | null = null\n\n        const rects = itemRectsRef.current\n        const scrollOffset = axis === 'x' ? container.scrollLeft : container.scrollTop\n        const borderOffset = axis === 'x' ? container.clientLeft : container.clientTop\n        const containerEdge = axis === 'x' ? containerRect.left : containerRect.top\n\n        const layoutSize = axis === 'x' ? container.offsetWidth : container.offsetHeight\n        const visualSize = axis === 'x' ? containerRect.width : containerRect.height\n        const scale = layoutSize > 0 ? visualSize / layoutSize : 1\n\n        for (let index = 0; index < rects.length; index++) {\n          const r = rects[index]\n          if (!r) continue\n\n          const contentPos = axis === 'x' ? r.left : r.top\n          const itemStart = containerEdge + (borderOffset + contentPos - scrollOffset) * scale\n          const itemSize = (axis === 'x' ? r.width : r.height) * scale\n          const itemEnd = itemStart + itemSize\n\n          if (mousePos >= itemStart && mousePos <= itemEnd) {\n            containingIndex = index\n          }\n\n          const itemCenter = itemStart + itemSize / 2\n          const distance = Math.abs(mousePos - itemCenter)\n\n          if (distance < closestDistance) {\n            closestDistance = distance\n            closestIndex = index\n          }\n        }\n\n        const next = containingIndex ?? closestIndex\n        if (next !== activeIndexRef.current) {\n          activeIndexRef.current = next\n          setActiveIndex(next)\n        }\n      })\n    },\n    [axis, containerRef, setActiveIndex],\n  )\n\n  const handleMouseEnter = useCallback(() => {\n    sessionRef.current += 1\n  }, [])\n\n  const handleMouseLeave = useCallback(() => {\n    if (rafIdRef.current !== null) {\n      cancelAnimationFrame(rafIdRef.current)\n      rafIdRef.current = null\n    }\n    activeIndexRef.current = null\n    setActiveIndex(null)\n  }, [setActiveIndex])\n\n  useEffect(() => {\n    return () => {\n      if (rafIdRef.current !== null) {\n        cancelAnimationFrame(rafIdRef.current)\n      }\n      if (remeasureRafIdRef.current !== null) {\n        cancelAnimationFrame(remeasureRafIdRef.current)\n      }\n    }\n  }, [])\n\n  return {\n    activeIndex,\n    setActiveIndex,\n    itemRects,\n    sessionRef,\n    handlers: {\n      onMouseMove: handleMouseMove,\n      onMouseEnter: handleMouseEnter,\n      onMouseLeave: handleMouseLeave,\n    },\n    registerItem,\n    measureItems,\n    highlightX,\n    highlightSize,\n    highlightOpacity,\n    axis,\n  }\n}\n\nexport interface ProximityHighlightProps {\n  highlightX: ReturnType<typeof useSpring>\n  highlightSize: MotionValue<number>\n  highlightOpacity: ReturnType<typeof useSpring>\n  axis: 'x' | 'y'\n  className?: string\n  style?: MotionStyle\n}\n\nexport function ProximityHighlight({\n  highlightX,\n  highlightSize,\n  highlightOpacity,\n  axis,\n  className,\n  style,\n}: ProximityHighlightProps) {\n  return (\n    <motion.div\n      aria-hidden=\"true\"\n      className={className}\n      style={{\n        ...style,\n        position: 'absolute',\n        inset: 0,\n        pointerEvents: 'none',\n        ...(axis === 'y'\n          ? { y: highlightX, height: highlightSize }\n          : { x: highlightX, width: highlightSize }),\n        opacity: highlightOpacity,\n      }}\n    />\n  )\n}\n\nexport function useRegisterProximityItem(\n  registerItem: (index: number, element: HTMLElement | null) => void,\n  index: number,\n  ref: RefObject<HTMLElement | null>,\n) {\n  useEffect(() => {\n    registerItem(index, ref.current)\n    return () => registerItem(index, null)\n  }, [index, registerItem, ref])\n}\n"
    }
  ],
  "type": "registry:ui"
}
