{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"combobox","type":"registry:component","title":"Combobox","description":"Searchable combobox with a morphing portal, grouped filtering, keyboard navigation, and controlled or uncontrolled state.","author":"Saurabh <saurabh10102@gmail.com>","dependencies":["clsx","lucide-react","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/combobox.tsx","type":"registry:component","target":"@components/motion/combobox.tsx","content":"\"use client\";\n// beui.dev/components/motion/combobox\n\nexport {\n  ComboboxContent,\n  type ComboboxContentProps,\n} from \"./combobox/content\";\nexport {\n  Combobox,\n  type ComboboxFilter,\n  type ComboboxProps,\n} from \"./combobox/context\";\nexport {\n  ComboboxEmpty,\n  type ComboboxEmptyProps,\n  ComboboxGroup,\n  type ComboboxGroupProps,\n  ComboboxItem,\n  type ComboboxItemProps,\n  ComboboxLabel,\n  type ComboboxLabelProps,\n  ComboboxList,\n  type ComboboxListProps,\n  ComboboxSeparator,\n  type ComboboxSeparatorProps,\n} from \"./combobox/list\";\nexport {\n  ComboboxInput,\n  type ComboboxInputProps,\n  ComboboxTrigger,\n  type ComboboxTriggerProps,\n  ComboboxValue,\n  type ComboboxValueProps,\n} from \"./combobox/trigger\";\n"},{"path":"components/motion/combobox/content.tsx","type":"registry:component","target":"@components/motion/combobox/content.tsx","content":"\"use client\";\n\nimport { motion, type Transition } from \"motion/react\";\nimport {\n  type CSSProperties,\n  type ReactNode,\n  useEffect,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { usePopoverPortalPosition } from \"@/components/motion/popover-position\";\nimport { cn } from \"@/lib/utils\";\nimport { useComboboxContext } from \"./context\";\n\ntype Side = \"top\" | \"bottom\";\ntype Align = \"start\" | \"center\" | \"end\";\n\n// The panel uses one weighted spring for both directions, so opening and\n// closing travel through the same detached geometry.\nconst COMBOBOX_MORPH: Transition = {\n  type: \"spring\",\n  duration: 0.5,\n  bounce: 0.22,\n};\nconst VIEWPORT_PADDING = 8;\n\nexport interface ComboboxContentProps {\n  children: ReactNode;\n  side?: Side;\n  align?: Align;\n  sideOffset?: number;\n  avoidCollisions?: boolean;\n  className?: string;\n}\n\nexport function ComboboxContent({\n  children,\n  side = \"bottom\",\n  align = \"start\",\n  sideOffset = 6,\n  avoidCollisions = true,\n  className,\n}: ComboboxContentProps) {\n  const context = useComboboxContext(\"ComboboxContent\");\n  const measureRef = useRef<HTMLDivElement>(null);\n  const [portalReady, setPortalReady] = useState(false);\n  const [actualSide, setActualSide] = useState<Side>(side);\n  const [morphReady, setMorphReady] = useState(false);\n  const layout = usePopoverPortalPosition(\n    context.triggerRef,\n    measureRef,\n    portalReady,\n  );\n\n  useEffect(() => setPortalReady(true), []);\n  useLayoutEffect(() => {\n    if (!portalReady) return;\n    const readyFrame = requestAnimationFrame(() => setMorphReady(true));\n    return () => cancelAnimationFrame(readyFrame);\n  }, [portalReady]);\n\n  useLayoutEffect(() => {\n    // Preserve the resolved side during exit, so top panels close upward.\n    if (!context.open || !layout) return;\n    if (!avoidCollisions) {\n      setActualSide(side);\n      return;\n    }\n    const below =\n      window.innerHeight - (layout.trigger.top + layout.trigger.height);\n    const above = layout.trigger.top;\n    if (\n      side === \"bottom\" &&\n      below < layout.content.height + sideOffset &&\n      above > below\n    )\n      setActualSide(\"top\");\n    else if (\n      side === \"top\" &&\n      above < layout.content.height + sideOffset &&\n      below > above\n    )\n      setActualSide(\"bottom\");\n    else setActualSide(side);\n  }, [avoidCollisions, context.open, layout, side, sideOffset]);\n\n  if (!portalReady) return null;\n\n  const triggerLeft = layout?.trigger.left ?? 0;\n  const triggerWidth = layout?.trigger.width ?? 0;\n  const contentWidth = layout?.content.width ?? triggerWidth;\n  const desiredLeft =\n    align === \"end\"\n      ? triggerLeft + triggerWidth - contentWidth\n      : align === \"center\"\n        ? triggerLeft + (triggerWidth - contentWidth) / 2\n        : triggerLeft;\n  const maxLeft = Math.max(\n    VIEWPORT_PADDING,\n    window.innerWidth - contentWidth - VIEWPORT_PADDING,\n  );\n  const left = Math.min(Math.max(desiredLeft, VIEWPORT_PADDING), maxLeft);\n  const surfaceHeight = layout?.content.height ?? 0;\n\n  return createPortal(\n    <motion.div\n      ref={context.contentRef}\n      data-combobox-content=\"\"\n      data-side={actualSide}\n      aria-hidden={!context.open}\n      inert={!context.open}\n      initial={false}\n      animate={{\n        height: context.open ? surfaceHeight : 0,\n        opacity: context.open ? 1 : 0,\n        y: context.open\n          ? actualSide === \"bottom\"\n            ? sideOffset\n            : -sideOffset\n          : 0,\n      }}\n      transition={\n        context.reduce || !morphReady ? { duration: 0 } : COMBOBOX_MORPH\n      }\n      style={\n        {\n          left,\n          top:\n            actualSide === \"bottom\" && layout\n              ? layout.trigger.top + layout.trigger.height\n              : undefined,\n          bottom:\n            actualSide === \"top\" && layout\n              ? window.innerHeight - layout.trigger.top\n              : undefined,\n          minWidth: triggerWidth,\n          pointerEvents: context.open ? \"auto\" : \"none\",\n          transformOrigin: actualSide === \"bottom\" ? \"top\" : \"bottom\",\n          visibility: layout ? \"visible\" : \"hidden\",\n          \"--combobox-trigger-width\": `${triggerWidth}px`,\n        } as CSSProperties\n      }\n      className={cn(\n        \"fixed z-[9999] w-(--combobox-trigger-width) overflow-hidden rounded-xl border border-border bg-background text-popover-foreground outline-none will-change-[height,transform]\",\n        className,\n      )}\n    >\n      <motion.div\n        ref={measureRef}\n        initial={false}\n        animate={{ opacity: context.open ? 1 : 0 }}\n        transition={\n          context.reduce || !morphReady ? { duration: 0 } : COMBOBOX_MORPH\n        }\n      >\n        {children}\n      </motion.div>\n    </motion.div>,\n    document.body,\n  );\n}\n"},{"path":"components/motion/combobox/context.tsx","type":"registry:component","target":"@components/motion/combobox/context.tsx","content":"\"use client\";\n\nimport { useReducedMotion } from \"motion/react\";\nimport {\n  createContext,\n  type MutableRefObject,\n  type ReactNode,\n  type Ref,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type RegisteredItem = {\n  value: string;\n  label: string;\n  keywords: string[];\n  disabled: boolean;\n  groupId: string | null;\n  id: string;\n  ref: MutableRefObject<HTMLButtonElement | null>;\n};\n\nexport type ComboboxFilter = (\n  value: string,\n  query: string,\n  keywords: string[],\n) => boolean;\n\nconst defaultFilter: ComboboxFilter = (value, query, keywords) => {\n  const needle = query.trim().toLocaleLowerCase();\n  if (!needle) return true;\n\n  const haystack = [value, ...keywords].join(\" \").toLocaleLowerCase();\n  let queryIndex = 0;\n  for (const character of haystack) {\n    if (character === needle[queryIndex]) queryIndex += 1;\n    if (queryIndex === needle.length) return true;\n  }\n  return false;\n};\n\nexport type ComboboxContextValue = {\n  open: boolean;\n  setOpen: (open: boolean, restoreFocus?: boolean) => void;\n  value: string | undefined;\n  select: (value: string) => void;\n  query: string;\n  setQuery: (query: string) => void;\n  activeValue: string | null;\n  setActiveValue: (value: string | null) => void;\n  moveActive: (direction: 1 | -1 | \"first\" | \"last\") => void;\n  selectActive: () => void;\n  registerItem: (item: RegisteredItem) => void;\n  unregisterItem: (value: string) => void;\n  labelFor: (value: string | undefined) => string | undefined;\n  isVisible: (value: string) => boolean;\n  hasVisibleItems: (groupId: string) => boolean;\n  visibleCount: number;\n  activeItemId: string | undefined;\n  triggerId: string;\n  listId: string;\n  inputId: string;\n  disabled: boolean;\n  reduce: boolean;\n  triggerRef: MutableRefObject<HTMLDivElement | null>;\n  contentRef: MutableRefObject<HTMLDivElement | null>;\n  inputRef: MutableRefObject<HTMLInputElement | null>;\n  activeLayoutId: string;\n};\n\nexport const ComboboxContext = createContext<ComboboxContextValue | null>(null);\nexport const ComboboxGroupContext = createContext<string | null>(null);\n\nexport function useComboboxContext(component: string) {\n  const context = useContext(ComboboxContext);\n  if (!context) throw new Error(`${component} must be used within <Combobox>`);\n  return context;\n}\n\nexport function mergeRefs<T>(...refs: Array<Ref<T> | undefined>) {\n  return (node: T | null) => {\n    for (const ref of refs) {\n      if (typeof ref === \"function\") ref(node);\n      else if (ref && typeof ref === \"object\")\n        (ref as MutableRefObject<T | null>).current = node;\n    }\n  };\n}\n\nexport interface ComboboxProps {\n  children: ReactNode;\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  query?: string;\n  defaultQuery?: string;\n  onQueryChange?: (query: string) => void;\n  filter?: ComboboxFilter;\n  disabled?: boolean;\n  className?: string;\n}\n\nexport function Combobox({\n  children,\n  value: controlledValue,\n  defaultValue,\n  onValueChange,\n  open: controlledOpen,\n  defaultOpen = false,\n  onOpenChange,\n  query: controlledQuery,\n  defaultQuery = \"\",\n  onQueryChange,\n  filter = defaultFilter,\n  disabled = false,\n  className,\n}: ComboboxProps) {\n  const reduce = useReducedMotion() ?? false;\n  const baseId = useId();\n  const rootRef = useRef<HTMLDivElement>(null);\n  const triggerRef = useRef<HTMLDivElement>(null);\n  const contentRef = useRef<HTMLDivElement>(null);\n  const inputRef = useRef<HTMLInputElement>(null);\n  const [internalValue, setInternalValue] = useState(defaultValue);\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const [internalQuery, setInternalQuery] = useState(defaultQuery);\n  const [activeValue, setActiveValue] = useState<string | null>(null);\n  const [items, setItems] = useState<Map<string, RegisteredItem>>(new Map());\n\n  const valueControlled = controlledValue !== undefined;\n  const openControlled = controlledOpen !== undefined;\n  const queryControlled = controlledQuery !== undefined;\n  const value = valueControlled ? controlledValue : internalValue;\n  const open = openControlled ? controlledOpen : internalOpen;\n  const query = queryControlled ? controlledQuery : internalQuery;\n\n  const updateQuery = useCallback(\n    (next: string) => {\n      if (!queryControlled) setInternalQuery(next);\n      onQueryChange?.(next);\n    },\n    [onQueryChange, queryControlled],\n  );\n\n  const updateOpen = useCallback(\n    (next: boolean, restoreFocus = false) => {\n      if (disabled && next) return;\n      if (!openControlled) setInternalOpen(next);\n      onOpenChange?.(next);\n      if (!next) updateQuery(\"\");\n      if (restoreFocus)\n        requestAnimationFrame(() =>\n          inputRef.current?.focus({ preventScroll: true }),\n        );\n    },\n    [disabled, onOpenChange, openControlled, updateQuery],\n  );\n\n  const registerItem = useCallback((item: RegisteredItem) => {\n    setItems((current) => {\n      const existing = current.get(item.value);\n      if (\n        existing?.label === item.label &&\n        existing.disabled === item.disabled &&\n        existing.id === item.id &&\n        existing.ref === item.ref &&\n        existing.groupId === item.groupId &&\n        existing.keywords.join(\"\\u0000\") === item.keywords.join(\"\\u0000\")\n      ) {\n        return current;\n      }\n      const next = new Map(current);\n      next.set(item.value, item);\n      return next;\n    });\n  }, []);\n\n  const unregisterItem = useCallback((itemValue: string) => {\n    setItems((current) => {\n      if (!current.has(itemValue)) return current;\n      const next = new Map(current);\n      next.delete(itemValue);\n      return next;\n    });\n  }, []);\n\n  const visibleItems = useMemo(\n    () =>\n      Array.from(items.values()).filter((item) =>\n        filter(item.value, query, [item.label, ...item.keywords]),\n      ),\n    [filter, items, query],\n  );\n  const enabledVisibleItems = useMemo(\n    () => visibleItems.filter((item) => !item.disabled),\n    [visibleItems],\n  );\n  const visibleValues = useMemo(\n    () => new Set(visibleItems.map((item) => item.value)),\n    [visibleItems],\n  );\n  const visibleGroupIds = useMemo(\n    () => new Set(visibleItems.map((item) => item.groupId)),\n    [visibleItems],\n  );\n\n  const select = useCallback(\n    (next: string) => {\n      if (items.get(next)?.disabled) return;\n      if (!valueControlled) setInternalValue(next);\n      onValueChange?.(next);\n      updateOpen(false, true);\n    },\n    [items, onValueChange, updateOpen, valueControlled],\n  );\n\n  const moveActive = useCallback(\n    (direction: 1 | -1 | \"first\" | \"last\") => {\n      if (!enabledVisibleItems.length) {\n        setActiveValue(null);\n        return;\n      }\n      if (direction === \"first\") {\n        setActiveValue(enabledVisibleItems[0].value);\n        return;\n      }\n      if (direction === \"last\") {\n        setActiveValue(enabledVisibleItems.at(-1)?.value ?? null);\n        return;\n      }\n\n      const currentIndex = enabledVisibleItems.findIndex(\n        (item) => item.value === activeValue,\n      );\n      const nextIndex =\n        currentIndex < 0\n          ? direction === 1\n            ? 0\n            : enabledVisibleItems.length - 1\n          : (currentIndex + direction + enabledVisibleItems.length) %\n            enabledVisibleItems.length;\n      setActiveValue(enabledVisibleItems[nextIndex].value);\n    },\n    [activeValue, enabledVisibleItems],\n  );\n\n  const selectActive = useCallback(() => {\n    if (activeValue) select(activeValue);\n  }, [activeValue, select]);\n\n  useEffect(() => {\n    if (!open) return;\n    const selectedVisible = value && visibleValues.has(value) ? value : null;\n    const activeVisible =\n      activeValue && visibleValues.has(activeValue) ? activeValue : null;\n    setActiveValue(\n      activeVisible ?? selectedVisible ?? enabledVisibleItems[0]?.value ?? null,\n    );\n  }, [activeValue, enabledVisibleItems, open, value, visibleValues]);\n\n  useEffect(() => {\n    if (!open) return;\n    requestAnimationFrame(() => inputRef.current?.focus({ preventScroll: true }));\n  }, [open]);\n\n  useEffect(() => {\n    if (!activeValue || !open) return;\n    const item = items.get(activeValue)?.ref.current;\n    const list = item?.closest<HTMLElement>(\"[role='listbox']\");\n    if (!item || !list) return;\n    const itemRect = item.getBoundingClientRect();\n    const listRect = list.getBoundingClientRect();\n    if (itemRect.top < listRect.top) list.scrollTop -= listRect.top - itemRect.top;\n    else if (itemRect.bottom > listRect.bottom)\n      list.scrollTop += itemRect.bottom - listRect.bottom;\n  }, [activeValue, items, open]);\n\n  useEffect(() => {\n    if (!open) return;\n    const isInside = (target: Node) =>\n      rootRef.current?.contains(target) || contentRef.current?.contains(target);\n    const onPointerDown = (event: PointerEvent) => {\n      if (!isInside(event.target as Node)) updateOpen(false);\n    };\n    const onFocusIn = (event: FocusEvent) => {\n      if (!isInside(event.target as Node)) updateOpen(false);\n    };\n    const onKeyDown = (event: KeyboardEvent) => {\n      if (event.key !== \"Escape\") return;\n      event.preventDefault();\n      updateOpen(false, true);\n    };\n    window.addEventListener(\"pointerdown\", onPointerDown);\n    window.addEventListener(\"focusin\", onFocusIn);\n    window.addEventListener(\"keydown\", onKeyDown);\n    return () => {\n      window.removeEventListener(\"pointerdown\", onPointerDown);\n      window.removeEventListener(\"focusin\", onFocusIn);\n      window.removeEventListener(\"keydown\", onKeyDown);\n    };\n  }, [open, updateOpen]);\n\n  const activeItem = activeValue ? items.get(activeValue) : undefined;\n  const context = useMemo<ComboboxContextValue>(\n    () => ({\n      open,\n      setOpen: updateOpen,\n      value,\n      select,\n      query,\n      setQuery: updateQuery,\n      activeValue,\n      setActiveValue,\n      moveActive,\n      selectActive,\n      registerItem,\n      unregisterItem,\n      labelFor: (itemValue) =>\n        itemValue === undefined ? undefined : items.get(itemValue)?.label,\n      isVisible: (itemValue) => !query.trim() || visibleValues.has(itemValue),\n      hasVisibleItems: (groupId) => visibleGroupIds.has(groupId),\n      visibleCount: visibleItems.length,\n      activeItemId: activeItem?.id,\n      triggerId: `${baseId}-trigger`,\n      listId: `${baseId}-list`,\n      inputId: `${baseId}-input`,\n      disabled,\n      reduce,\n      triggerRef,\n      contentRef,\n      inputRef,\n      activeLayoutId: `${baseId}-active`,\n    }),\n    [\n      activeItem?.id,\n      activeValue,\n      baseId,\n      disabled,\n      items,\n      moveActive,\n      open,\n      query,\n      reduce,\n      registerItem,\n      select,\n      selectActive,\n      unregisterItem,\n      updateOpen,\n      updateQuery,\n      value,\n      visibleItems.length,\n      visibleGroupIds,\n      visibleValues,\n    ],\n  );\n\n  return (\n    <ComboboxContext.Provider value={context}>\n      <div ref={rootRef} className={cn(\"relative w-full\", className)}>\n        {children}\n      </div>\n    </ComboboxContext.Provider>\n  );\n}\n"},{"path":"components/motion/combobox/list.tsx","type":"registry:component","target":"@components/motion/combobox/list.tsx","content":"\"use client\";\n\nimport { Check } from \"lucide-react\";\nimport { motion } from \"motion/react\";\nimport {\n  type ReactNode,\n  useContext,\n  useId,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n} from \"react\";\nimport { EASE_OUT, SPRING_LAYOUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\nimport { ComboboxGroupContext, useComboboxContext } from \"./context\";\n\nexport interface ComboboxListProps {\n  children: ReactNode;\n  ariaLabel?: string;\n  className?: string;\n}\n\nexport function ComboboxList({\n  children,\n  ariaLabel = \"Options\",\n  className,\n}: ComboboxListProps) {\n  const context = useComboboxContext(\"ComboboxList\");\n  return (\n    <div\n      id={context.listId}\n      role=\"listbox\"\n      aria-label={ariaLabel}\n      className={cn(\n        \"max-h-64 overflow-y-auto overscroll-contain p-1.5 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\",\n        className,\n      )}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport interface ComboboxGroupProps {\n  children: ReactNode;\n  className?: string;\n}\n\nexport function ComboboxGroup({ children, className }: ComboboxGroupProps) {\n  const context = useComboboxContext(\"ComboboxGroup\");\n  const groupId = useId();\n  return (\n    <ComboboxGroupContext.Provider value={groupId}>\n      <fieldset\n        hidden={!context.hasVisibleItems(groupId)}\n        className={cn(\"m-0 min-w-0 border-0 p-0 py-0.5\", className)}\n      >\n        {children}\n      </fieldset>\n    </ComboboxGroupContext.Provider>\n  );\n}\n\nexport interface ComboboxLabelProps {\n  children: ReactNode;\n  className?: string;\n}\n\nexport function ComboboxLabel({ children, className }: ComboboxLabelProps) {\n  const groupId = useContext(ComboboxGroupContext);\n  const labelClassName = cn(\n    \"w-full px-2 py-1.5 text-[0.68rem] font-medium uppercase tracking-[0.12em] text-muted-foreground\",\n    className,\n  );\n  return groupId ? (\n    <legend className={labelClassName}>{children}</legend>\n  ) : (\n    <div className={labelClassName}>{children}</div>\n  );\n}\n\nexport interface ComboboxItemProps {\n  value: string;\n  children: ReactNode;\n  textValue?: string;\n  keywords?: string[];\n  disabled?: boolean;\n  onSelect?: (value: string) => void;\n  className?: string;\n}\n\nexport function ComboboxItem({\n  value,\n  children,\n  textValue,\n  keywords = [],\n  disabled = false,\n  onSelect,\n  className,\n}: ComboboxItemProps) {\n  const context = useComboboxContext(\"ComboboxItem\");\n  const groupId = useContext(ComboboxGroupContext);\n  const id = useId();\n  const itemRef = useRef<HTMLButtonElement>(null);\n  const label = textValue ?? (typeof children === \"string\" ? children : value);\n  const visible = context.isVisible(value);\n  const active = context.activeValue === value;\n  const selected = context.value === value;\n  const keywordKey = keywords.join(\"\\u0000\");\n  const normalizedKeywords = useMemo(\n    () => (keywordKey ? keywordKey.split(\"\\u0000\") : []),\n    [keywordKey],\n  );\n  const { registerItem, unregisterItem } = context;\n\n  useLayoutEffect(() => {\n    registerItem({\n      value,\n      label,\n      keywords: normalizedKeywords,\n      disabled,\n      groupId,\n      id,\n      ref: itemRef,\n    });\n    return () => unregisterItem(value);\n  }, [\n    disabled,\n    groupId,\n    id,\n    label,\n    normalizedKeywords,\n    registerItem,\n    unregisterItem,\n    value,\n  ]);\n\n  if (!visible) return null;\n\n  return (\n    <button\n      ref={itemRef}\n      id={id}\n      type=\"button\"\n      role=\"option\"\n      aria-selected={selected}\n      disabled={disabled}\n      tabIndex={-1}\n      data-combobox-item=\"\"\n      data-active={active || undefined}\n      data-selected={selected || undefined}\n      onPointerMove={() => {\n        if (!disabled) context.setActiveValue(value);\n      }}\n      onPointerDown={(event) => event.preventDefault()}\n      onClick={() => {\n        if (disabled) return;\n        onSelect?.(value);\n        context.select(value);\n      }}\n      className={cn(\n        \"relative isolate flex w-full items-center gap-2 rounded-lg px-2 py-2 text-left text-sm outline-none transition-colors duration-150\",\n        active ? \"text-foreground\" : \"text-muted-foreground\",\n        \"disabled:pointer-events-none disabled:opacity-45\",\n        className,\n      )}\n    >\n      {active ? (\n        <motion.span\n          aria-hidden\n          layoutId={context.activeLayoutId}\n          className=\"absolute inset-0 -z-10 rounded-lg bg-muted\"\n          transition={context.reduce ? { duration: 0 } : SPRING_LAYOUT}\n        />\n      ) : null}\n      <span className=\"min-w-0 flex-1\">{children}</span>\n      <motion.span\n        aria-hidden\n        initial={false}\n        animate={{\n          opacity: selected ? 1 : 0,\n          transform: selected ? \"scale(1)\" : \"scale(0.82)\",\n        }}\n        transition={\n          context.reduce\n            ? { duration: 0 }\n            : { duration: 0.14, ease: EASE_OUT }\n        }\n        className=\"grid size-5 shrink-0 place-items-center text-foreground\"\n      >\n        <Check className=\"size-4\" />\n      </motion.span>\n    </button>\n  );\n}\n\nexport interface ComboboxEmptyProps {\n  children?: ReactNode;\n  className?: string;\n}\n\nexport function ComboboxEmpty({\n  children = \"No options found.\",\n  className,\n}: ComboboxEmptyProps) {\n  const context = useComboboxContext(\"ComboboxEmpty\");\n  if (context.visibleCount > 0) return null;\n  return (\n    <div\n      role=\"status\"\n      className={cn(\n        \"px-3 py-8 text-center text-sm text-muted-foreground\",\n        className,\n      )}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport interface ComboboxSeparatorProps {\n  className?: string;\n}\n\nexport function ComboboxSeparator({ className }: ComboboxSeparatorProps) {\n  return (\n    <div\n      aria-hidden\n      className={cn(\"-mx-1 my-1 h-px bg-border\", className)}\n    />\n  );\n}\n"},{"path":"components/motion/combobox/trigger.tsx","type":"registry:component","target":"@components/motion/combobox/trigger.tsx","content":"\"use client\";\n\nimport { ChevronsUpDown, Search } from \"lucide-react\";\nimport type {\n  InputHTMLAttributes,\n  KeyboardEvent as ReactKeyboardEvent,\n  ReactNode,\n  Ref,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { mergeRefs, useComboboxContext } from \"./context\";\n\nexport interface ComboboxTriggerProps {\n  children: ReactNode;\n  className?: string;\n}\n\nexport function ComboboxTrigger({ children, className }: ComboboxTriggerProps) {\n  const context = useComboboxContext(\"ComboboxTrigger\");\n\n  return (\n    <div\n      ref={context.triggerRef}\n      id={context.triggerId}\n      data-state={context.open ? \"open\" : \"closed\"}\n      onPointerDown={(event) => {\n        if (context.disabled || event.target === context.inputRef.current) return;\n        event.preventDefault();\n        context.inputRef.current?.focus({ preventScroll: true });\n        context.setOpen(true);\n      }}\n      className={cn(\n        \"relative z-20 flex h-10 w-full min-w-52 cursor-text items-center justify-between gap-3 rounded-xl border border-border bg-transparent px-3 text-sm text-foreground transition-[border-color] hover:border-(--color-border-strong)\",\n        \"focus-within:ring-2 focus-within:ring-foreground/20\",\n        context.disabled && \"pointer-events-none opacity-50\",\n        className,\n      )}\n    >\n      <span className=\"min-w-0 flex-1 text-left\">{children}</span>\n      <span aria-hidden className=\"shrink-0 text-muted-foreground\">\n        <ChevronsUpDown className=\"size-4\" />\n      </span>\n    </div>\n  );\n}\n\nexport interface ComboboxValueProps {\n  placeholder?: ReactNode;\n  children?:\n    | ReactNode\n    | ((value: string | undefined, label: string | undefined) => ReactNode);\n  className?: string;\n}\n\nexport function ComboboxValue({\n  placeholder = \"Select an option\",\n  children,\n  className,\n}: ComboboxValueProps) {\n  const context = useComboboxContext(\"ComboboxValue\");\n  const label = context.labelFor(context.value);\n  const content =\n    typeof children === \"function\"\n      ? children(context.value, label)\n      : children ?? label ?? placeholder;\n\n  return (\n    <span\n      className={cn(\n        \"block truncate\",\n        context.value === undefined\n          ? \"text-muted-foreground\"\n          : \"text-foreground\",\n        className,\n      )}\n    >\n      {content}\n    </span>\n  );\n}\n\nexport interface ComboboxInputProps\n  extends Omit<\n    InputHTMLAttributes<HTMLInputElement>,\n    \"defaultValue\" | \"value\"\n  > {\n  ref?: Ref<HTMLInputElement>;\n  wrapperClassName?: string;\n}\n\nexport function ComboboxInput({\n  ref,\n  className,\n  wrapperClassName,\n  \"aria-label\": ariaLabel = \"Search options\",\n  onChange,\n  onClick,\n  onFocus,\n  onKeyDown,\n  onPointerDown,\n  placeholder = \"Search…\",\n  ...props\n}: ComboboxInputProps) {\n  const context = useComboboxContext(\"ComboboxInput\");\n  const selectedLabel = context.labelFor(context.value);\n\n  const handleKeyDown = (event: ReactKeyboardEvent<HTMLInputElement>) => {\n    onKeyDown?.(event);\n    if (event.defaultPrevented) return;\n\n    if (event.key === \"ArrowDown\") {\n      event.preventDefault();\n      context.setOpen(true);\n      context.moveActive(1);\n    } else if (event.key === \"ArrowUp\") {\n      event.preventDefault();\n      context.setOpen(true);\n      context.moveActive(-1);\n    } else if (event.key === \"Home\" && context.open) {\n      event.preventDefault();\n      context.moveActive(\"first\");\n    } else if (event.key === \"End\" && context.open) {\n      event.preventDefault();\n      context.moveActive(\"last\");\n    } else if (event.key === \"Enter\") {\n      event.preventDefault();\n      if (context.open) context.selectActive();\n      else context.setOpen(true);\n    } else if (event.key === \"Escape\" && context.open) {\n      event.preventDefault();\n      context.setOpen(false, true);\n    }\n  };\n\n  return (\n    <div\n      className={cn(\n        \"flex min-w-0 flex-1 items-center gap-2\",\n        wrapperClassName,\n      )}\n    >\n      <Search aria-hidden className=\"size-4 shrink-0 text-muted-foreground\" />\n      <input\n        {...props}\n        ref={mergeRefs(ref, context.inputRef)}\n        id={context.inputId}\n        role=\"combobox\"\n        aria-label={ariaLabel}\n        aria-autocomplete=\"list\"\n        aria-expanded={context.open}\n        aria-controls={context.listId}\n        aria-activedescendant={\n          context.open ? context.activeItemId : undefined\n        }\n        autoComplete=\"off\"\n        disabled={context.disabled}\n        value={context.open ? context.query : (selectedLabel ?? \"\")}\n        placeholder={placeholder}\n        onPointerDown={(event) => {\n          onPointerDown?.(event);\n          if (event.defaultPrevented || context.open) return;\n          event.preventDefault();\n          context.inputRef.current?.focus({ preventScroll: true });\n          context.setOpen(true);\n        }}\n        onFocus={(event) => {\n          context.setOpen(true);\n          onFocus?.(event);\n        }}\n        onClick={(event) => {\n          context.setOpen(true);\n          onClick?.(event);\n        }}\n        onChange={(event) => {\n          context.setOpen(true);\n          context.setQuery(event.target.value);\n          onChange?.(event);\n        }}\n        onKeyDown={handleKeyDown}\n        className={cn(\n          \"h-10 min-w-0 flex-1 bg-transparent text-sm text-foreground outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed\",\n          className,\n        )}\n      />\n    </div>\n  );\n}\n"},{"path":"components/motion/popover-position.ts","type":"registry:component","target":"@components/motion/popover-position.ts","content":"\"use client\";\n\nimport {\n  type MutableRefObject,\n  useCallback,\n  useLayoutEffect,\n  useState,\n} from \"react\";\n\nexport type PortalLayout = {\n  trigger: {\n    left: number;\n    top: number;\n    width: number;\n    height: number;\n  };\n  content: {\n    width: number;\n    height: number;\n  };\n};\n\nfunction sameLayout(a: PortalLayout | null, b: PortalLayout) {\n  return (\n    a?.trigger.left === b.trigger.left &&\n    a.trigger.top === b.trigger.top &&\n    a.trigger.width === b.trigger.width &&\n    a.trigger.height === b.trigger.height &&\n    a.content.width === b.content.width &&\n    a.content.height === b.content.height\n  );\n}\n\n/** Measures a trigger and portalled panel in viewport coordinates. */\nexport function usePopoverPortalPosition<\n  TriggerElement extends HTMLElement,\n  ContentElement extends HTMLElement,\n>(\n  triggerRef: MutableRefObject<TriggerElement | null>,\n  contentRef: MutableRefObject<ContentElement | null>,\n  active: boolean,\n) {\n  const [layout, setLayout] = useState<PortalLayout | null>(null);\n\n  const update = useCallback(() => {\n    const trigger = triggerRef.current;\n    const content = contentRef.current;\n    if (!trigger || !content) return;\n\n    const rect = trigger.getBoundingClientRect();\n    const next: PortalLayout = {\n      trigger: {\n        left: rect.left,\n        top: rect.top,\n        width: rect.width,\n        height: rect.height,\n      },\n      content: {\n        width: content.offsetWidth,\n        height: content.offsetHeight,\n      },\n    };\n    setLayout((current) => (sameLayout(current, next) ? current : next));\n  }, [contentRef, triggerRef]);\n\n  useLayoutEffect(() => {\n    update();\n    if (!active) return;\n\n    const trigger = triggerRef.current;\n    const content = contentRef.current;\n    const observer = new ResizeObserver(update);\n    if (trigger) observer.observe(trigger);\n    if (content) observer.observe(content);\n\n    window.addEventListener(\"scroll\", update, true);\n    window.addEventListener(\"resize\", update);\n    return () => {\n      observer.disconnect();\n      window.removeEventListener(\"scroll\", update, true);\n      window.removeEventListener(\"resize\", update);\n    };\n  }, [active, contentRef, triggerRef, update]);\n\n  return layout;\n}\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"},{"path":"lib/ease.ts","type":"registry:lib","target":"@lib/ease.ts","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n\n/** Dragged handles and fills (sliders) — critically damped `useSpring` config,\n * so the value follows the pointer butterily and never rebounds off an end. */\nexport const SPRING_GLIDE = {\n  stiffness: 700,\n  damping: 50,\n  mass: 0.5,\n} as const;\n"}]}