{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "animated-combobox",
  "type": "registry:ui",
  "title": "Animated combobox",
  "description": "Searches and selects a changing result set while preserving keyboard position.",
  "dependencies": [
    "motion"
  ],
  "categories": [
    "forms-input"
  ],
  "docs": "https://motion-lexicon.pages.dev/en/components/animated-combobox/",
  "meta": {
    "engines": [
      "motion"
    ],
    "runtimeCost": "light",
    "signature": "Highlight and result ordering preserve focus continuity",
    "sceneFamily": "product-mono",
    "motionRole": "ui",
    "primaryState": "Expanded project search results",
    "assetProvenance": "self-contained"
  },
  "files": [
    {
      "path": "src/registry/components/animated-combobox.tsx",
      "type": "registry:ui",
      "target": "components/motion-lexicon/animated-combobox.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useId, useRef, useState } from \"react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\n\nexport type AnimatedComboboxOption = { id: string; label: string; detail?: string };\nexport type AnimatedComboboxProps = { options: readonly AnimatedComboboxOption[]; label?: string; placeholder?: string; onChange?: (option: AnimatedComboboxOption | null) => void; className?: string };\n\nexport function AnimatedCombobox({ options, label = \"Choose an option\", placeholder = \"Search…\", onChange, className = \"\" }: AnimatedComboboxProps) {\n  const reduced = useReducedMotion() === true; const inputId = useId(); const listId = useId(); const rootRef = useRef<HTMLDivElement>(null); const [open, setOpen] = useState(false); const [query, setQuery] = useState(\"\"); const [active, setActive] = useState(0); const [selected, setSelected] = useState<AnimatedComboboxOption | null>(null); const filtered = options.filter((item) => `${item.label} ${item.detail ?? \"\"}`.toLowerCase().includes(query.toLowerCase()));\n  useEffect(() => { const close = (event: MouseEvent) => { if (!rootRef.current?.contains(event.target as Node)) setOpen(false); }; document.addEventListener(\"mousedown\", close); return () => document.removeEventListener(\"mousedown\", close); }, []);\n  const pick = (option: AnimatedComboboxOption) => { setSelected(option); setQuery(option.label); setOpen(false); onChange?.(option); };\n  return <div ref={rootRef} className={`relative ${className}`}><label htmlFor={inputId} className=\"block text-[12px] font-medium text-[#292929]\">{label}</label><input id={inputId} role=\"combobox\" aria-expanded={open} aria-controls={listId} aria-autocomplete=\"list\" aria-activedescendant={open && filtered[active] ? `${listId}-${filtered[active].id}` : undefined} value={query} placeholder={placeholder} onFocus={() => setOpen(true)} onChange={(event) => { setQuery(event.target.value); setActive(0); setOpen(true); }} onKeyDown={(event) => { if (event.key === \"ArrowDown\") { event.preventDefault(); setOpen(true); setActive((value) => Math.min(value + 1, Math.max(0, filtered.length - 1))); } if (event.key === \"ArrowUp\") { event.preventDefault(); setOpen(true); setActive((value) => Math.max(0, value - 1)); } if (event.key === \"Enter\" && open && filtered[active]) { event.preventDefault(); pick(filtered[active]); } if (event.key === \"Escape\") setOpen(false); }} className=\"mt-1.5 min-h-11 w-full rounded-[10px] border border-black/[.12] bg-white px-3 text-[13px] outline-none placeholder:text-neutral-400 focus-visible:ring-2 focus-visible:ring-[#4568FF]\" />{selected ? <span className=\"mt-1 block text-[10px] text-neutral-500\">Selected: {selected.label}</span> : null}<AnimatePresence>{open ? <motion.ul id={listId} role=\"listbox\" initial={reduced ? false : { opacity: 0, y: -6 }} animate={{ opacity: 1, y: 0 }} exit={reduced ? undefined : { opacity: 0, y: -4 }} transition={{ duration: reduced ? 0 : .16 }} className=\"absolute z-20 mt-1 max-h-52 w-full overflow-auto rounded-[10px] border border-black/[.1] bg-white p-1 shadow-[0_16px_35px_-20px_rgba(25,25,25,.35)]\">{filtered.length ? filtered.map((option, optionIndex) => <li key={option.id} id={`${listId}-${option.id}`} role=\"option\" aria-selected={selected?.id === option.id}><button type=\"button\" onMouseEnter={() => setActive(optionIndex)} onClick={() => pick(option)} className={`min-h-11 w-full rounded-lg px-3 py-2 text-left outline-none focus-visible:ring-2 focus-visible:ring-[#4568FF] ${active === optionIndex ? \"bg-[#edf0ff]\" : \"hover:bg-neutral-50\"}`}><strong className=\"block text-[12px] text-[#292929]\">{option.label}</strong>{option.detail ? <span className=\"mt-0.5 block text-[10px] text-neutral-500\">{option.detail}</span> : null}</button></li>) : <li role=\"status\" className=\"px-3 py-4 text-[12px] text-neutral-500\">No matches found.</li>}</motion.ul> : null}</AnimatePresence></div>;\n}\n"
    }
  ]
}
