{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "combobox-styled",
  "title": "Combobox essentials",
  "description": "Styled combobox: the essentials set. 5 decorative combobox variations (SimpleCombobox, GlassCombobox, IconCombobox, TagsCombobox, InlineCombobox) on the ai2 token system, powered by framer-motion, reduced-motion aware and sized sm to xl. Part of the free styled layer.",
  "dependencies": [
    "motion@^12.42.2",
    "lucide-react@^1.23.0"
  ],
  "registryDependencies": [
    "@ai2/tokens"
  ],
  "files": [
    {
      "path": "registry/ai2/styled/combobox-styled.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Check, ChevronsUpDown, Hash, Search, X } from \"lucide-react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/* Combobox family: 5 decorative SELF-CONTAINED searchable selects. NO radix, NO\n   portal, NO cmdk. Each export is a complete combobox: a trigger showing the\n   current value; clicking it opens a panel absolutely positioned BELOW it (a\n   search <input> + a filtered list). Filtering is a case-insensitive `includes`\n   on the label. The arrow keys (Up/Down) move the highlighted index, Enter\n   selects, Escape or an outside click closes. Controlled (value/onValueChange) +\n   uncontrolled (defaultValue). AnimatePresence fade+scale; no animation under\n   reduced motion. Color comes ONLY from tokens, via alpha color-mix. */\n\nexport type StyledSize = \"sm\" | \"md\" | \"lg\" | \"xl\"\n\nconst triggerHeight: Record<StyledSize, string> = {\n  sm: \"h-8\",\n  md: \"h-9\",\n  lg: \"h-10\",\n  xl: \"h-12\",\n}\n\nconst panelWidth: Record<StyledSize, string> = {\n  sm: \"w-56\",\n  md: \"w-64\",\n  lg: \"w-72\",\n  xl: \"w-80\",\n}\n\nexport interface ComboboxOption {\n  value: string\n  label: React.ReactNode\n}\n\ninterface ComboboxProps {\n  className?: string\n  size?: StyledSize\n  placeholder?: string\n  options?: ComboboxOption[]\n  value?: string\n  defaultValue?: string\n  onValueChange?: (v: string) => void\n}\n\n/* Varsayilan 6 secenek: prop'suz render eder. */\nconst defaultOptions: ComboboxOption[] = [\n  { value: \"next\", label: \"Next.js\" },\n  { value: \"react\", label: \"React\" },\n  { value: \"vue\", label: \"Vue\" },\n  { value: \"svelte\", label: \"Svelte\" },\n  { value: \"solid\", label: \"Solid\" },\n  { value: \"astro\", label: \"Astro\" },\n]\n\n/* The label can be a ReactNode; flatten it to plain text for filtering. */\nfunction nodeText(node: React.ReactNode): string {\n  if (node === null || node === undefined || typeof node === \"boolean\") return \"\"\n  if (typeof node === \"string\" || typeof node === \"number\") return String(node)\n  if (Array.isArray(node)) return node.map(nodeText).join(\"\")\n  if (React.isValidElement(node)) {\n    return nodeText((node.props as { children?: React.ReactNode }).children)\n  }\n  return \"\"\n}\n\n/* Sorguyu filtreye ceviren ortak yardimci. */\nfunction filterOptions(options: ComboboxOption[], query: string): ComboboxOption[] {\n  const q = query.trim().toLowerCase()\n  if (q.length === 0) return options\n  return options.filter((o) => nodeText(o.label).toLowerCase().includes(q))\n}\n\nconst rootBase = \"relative\"\n\nconst triggerBase =\n  \"flex w-full items-center justify-between gap-2 rounded-lg border border-field-border bg-transparent px-3 text-sm text-foreground shadow-xs outline-none transition-colors hover:bg-[color-mix(in_oklab,var(--color-foreground)_4%,transparent)] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground [&_i]:text-base [&_i]:leading-none [&_i]:text-muted-foreground\"\n\nconst panelBase =\n  \"absolute left-0 right-0 top-full z-50 mt-2 origin-top overflow-hidden rounded-xl border border-border bg-popover text-popover-foreground shadow-lg outline-none\"\n\nconst inputRowBase = \"flex items-center gap-2 border-b border-border px-3\"\n\nconst inputBase =\n  \"h-10 w-full bg-transparent py-2.5 text-sm text-popover-foreground outline-none placeholder:text-muted-foreground\"\n\nconst listBase = \"max-h-60 overflow-y-auto p-1.5\"\n\nconst optionBase =\n  \"flex cursor-pointer select-none items-center gap-2 rounded-lg px-2.5 py-2 text-sm outline-none transition-colors [&_svg]:size-4 [&_svg]:shrink-0 [&_i]:text-base [&_i]:leading-none\"\n\nconst emptyBase = \"py-6 text-center text-sm text-muted-foreground\"\n\nconst panelMotion = {\n  initial: { opacity: 0, scale: 0.98, y: -6 },\n  animate: { opacity: 1, scale: 1, y: 0 },\n  exit: { opacity: 0, scale: 0.98, y: -6 },\n}\n\n/* Shared state: controlled/uncontrolled value plus open state plus query plus highlighted index plus keyboard handling plus closing on outside click and Escape. */\nfunction useSingleCombobox(props: ComboboxProps) {\n  const { options = defaultOptions, value, defaultValue, onValueChange } = props\n  const isControlled = value !== undefined\n  const [internal, setInternal] = React.useState<string | undefined>(defaultValue)\n  const current = isControlled ? value : internal\n\n  const [open, setOpen] = React.useState(false)\n  const [query, setQuery] = React.useState(\"\")\n  const [active, setActive] = React.useState(0)\n\n  const wrapperRef = React.useRef<HTMLDivElement>(null)\n  const inputRef = React.useRef<HTMLInputElement>(null)\n\n  const filtered = React.useMemo(() => filterOptions(options, query), [options, query])\n\n  React.useEffect(() => {\n    setActive((prev) => (prev >= filtered.length ? 0 : prev))\n  }, [filtered.length])\n\n  const close = React.useCallback(() => {\n    setOpen(false)\n    setQuery(\"\")\n    setActive(0)\n  }, [])\n\n  React.useEffect(() => {\n    if (!open) return\n    inputRef.current?.focus()\n    const onKey = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") close()\n    }\n    const onPointer = (e: PointerEvent) => {\n      const node = wrapperRef.current\n      if (node && e.target instanceof Node && !node.contains(e.target)) close()\n    }\n    window.addEventListener(\"keydown\", onKey)\n    window.addEventListener(\"pointerdown\", onPointer)\n    return () => {\n      window.removeEventListener(\"keydown\", onKey)\n      window.removeEventListener(\"pointerdown\", onPointer)\n    }\n  }, [open, close])\n\n  const commit = React.useCallback(\n    (next: string) => {\n      if (!isControlled) setInternal(next)\n      onValueChange?.(next)\n      close()\n    },\n    [isControlled, onValueChange, close]\n  )\n\n  const onInputKeyDown = React.useCallback(\n    (e: React.KeyboardEvent<HTMLInputElement>) => {\n      if (e.key === \"ArrowDown\") {\n        e.preventDefault()\n        setActive((prev) => (filtered.length === 0 ? 0 : (prev + 1) % filtered.length))\n      } else if (e.key === \"ArrowUp\") {\n        e.preventDefault()\n        setActive((prev) =>\n          filtered.length === 0 ? 0 : (prev - 1 + filtered.length) % filtered.length\n        )\n      } else if (e.key === \"Enter\") {\n        e.preventDefault()\n        const item = filtered[active]\n        if (item) commit(item.value)\n      } else if (e.key === \"Escape\") {\n        e.preventDefault()\n        close()\n      }\n    },\n    [filtered, active, commit, close]\n  )\n\n  const selected = React.useMemo(\n    () => (current === undefined ? undefined : options.find((o) => o.value === current)),\n    [options, current]\n  )\n\n  return {\n    options,\n    current,\n    selected,\n    open,\n    setOpen,\n    query,\n    setQuery,\n    active,\n    setActive,\n    filtered,\n    commit,\n    close,\n    onInputKeyDown,\n    wrapperRef,\n    inputRef,\n  }\n}\n\n/* Panel body: the search row plus the filtered list plus the empty state. Through renderOption every variant draws the option row its own way. */\nfunction ComboboxPanel({\n  state,\n  size,\n  placeholder,\n  panelClassName,\n  inputRowClassName,\n  activeClassName,\n  renderOption,\n  reduce,\n}: {\n  state: ReturnType<typeof useSingleCombobox>\n  size: StyledSize\n  placeholder: string\n  panelClassName?: string\n  inputRowClassName?: string\n  activeClassName: string\n  renderOption: (option: ComboboxOption, isActive: boolean, isSelected: boolean) => React.ReactNode\n  reduce: boolean\n}) {\n  const { open, query, setQuery, active, setActive, filtered, commit, current, onInputKeyDown, inputRef } =\n    state\n  return (\n    <AnimatePresence>\n      {open ? (\n        <motion.div\n          role=\"listbox\"\n          className={cn(panelBase, panelWidth[size], panelClassName)}\n          initial={reduce ? { opacity: 0 } : panelMotion.initial}\n          animate={reduce ? { opacity: 1 } : panelMotion.animate}\n          exit={reduce ? { opacity: 0 } : panelMotion.exit}\n          transition={reduce ? { duration: 0 } : { duration: 0.16, ease: \"easeOut\" }}\n        >\n          <div className={cn(inputRowBase, inputRowClassName)}>\n            <Search className=\"size-4 shrink-0 text-muted-foreground\" />\n            <input\n              ref={inputRef}\n              aria-autocomplete=\"list\"\n              value={query}\n              placeholder={placeholder}\n              onChange={(e) => setQuery(e.target.value)}\n              onKeyDown={onInputKeyDown}\n              className={inputBase}\n            />\n          </div>\n          <div className={listBase}>\n            {filtered.length === 0 ? (\n              <p className={emptyBase}>No results found.</p>\n            ) : (\n              filtered.map((option, i) => {\n                const isActive = i === active\n                const isSelected = option.value === current\n                return (\n                  <div\n                    key={option.value}\n                    role=\"option\"\n                    aria-selected={isSelected}\n                    onMouseEnter={() => setActive(i)}\n                    onClick={() => commit(option.value)}\n                    className={cn(optionBase, (isActive || isSelected) && activeClassName)}\n                  >\n                    {renderOption(option, isActive, isSelected)}\n                  </div>\n                )\n              })\n            )}\n          </div>\n        </motion.div>\n      ) : null}\n    </AnimatePresence>\n  )\n}\n\n/* SimpleCombobox: a clean bordered trigger + a plain list, with a check on the\n   selected row. */\nexport function SimpleCombobox({\n  className,\n  size = \"md\",\n  placeholder = \"Search...\",\n  options,\n  value,\n  defaultValue,\n  onValueChange,\n}: ComboboxProps) {\n  const state = useSingleCombobox({ options, value, defaultValue, onValueChange })\n  const reduce = useReducedMotion() ?? false\n  const { open, setOpen, selected } = state\n\n  return (\n    <div ref={state.wrapperRef} data-slot=\"styled-combobox\" className={cn(rootBase, panelWidth[size], className)}>\n      <button\n        type=\"button\"\n        role=\"combobox\"\n        aria-expanded={open}\n        aria-haspopup=\"listbox\"\n        onClick={() => setOpen(!open)}\n        className={cn(triggerBase, triggerHeight[size])}\n      >\n        <span className={cn(\"truncate\", !selected && \"text-muted-foreground\")}>\n          {selected ? selected.label : placeholder}\n        </span>\n        <ChevronsUpDown className=\"opacity-70\" />\n      </button>\n      <ComboboxPanel\n        state={state}\n        size={size}\n        placeholder={placeholder}\n        activeClassName=\"bg-accent text-accent-foreground\"\n        reduce={reduce}\n        renderOption={(option, _isActive, isSelected) => (\n          <>\n            <span className=\"flex-1 truncate\">{option.label}</span>\n            {isSelected ? <Check className=\"text-foreground\" /> : null}\n          </>\n        )}\n      />\n    </div>\n  )\n}\n\n/* GlassCombobox: buzlu cam trigger + panel, backdrop-blur. */\nexport function GlassCombobox({\n  className,\n  size = \"md\",\n  placeholder = \"Search...\",\n  options,\n  value,\n  defaultValue,\n  onValueChange,\n}: ComboboxProps) {\n  const state = useSingleCombobox({ options, value, defaultValue, onValueChange })\n  const reduce = useReducedMotion() ?? false\n  const { open, setOpen, selected } = state\n\n  const glassSurface =\n    \"bg-[color-mix(in_oklab,var(--color-popover)_75%,transparent)] supports-[backdrop-filter]:bg-[color-mix(in_oklab,var(--color-popover)_55%,transparent)] supports-[backdrop-filter]:backdrop-blur-xl\"\n\n  return (\n    <div ref={state.wrapperRef} data-slot=\"styled-combobox\" className={cn(rootBase, panelWidth[size], className)}>\n      <button\n        type=\"button\"\n        role=\"combobox\"\n        aria-expanded={open}\n        aria-haspopup=\"listbox\"\n        onClick={() => setOpen(!open)}\n        className={cn(\n          triggerBase,\n          triggerHeight[size],\n          \"border-border\",\n          glassSurface,\n          \"shadow-[inset_0_1px_0_color-mix(in_oklab,var(--color-foreground)_12%,transparent)]\"\n        )}\n      >\n        <span className={cn(\"truncate\", !selected && \"text-muted-foreground\")}>\n          {selected ? selected.label : placeholder}\n        </span>\n        <ChevronsUpDown className=\"opacity-70\" />\n      </button>\n      <ComboboxPanel\n        state={state}\n        size={size}\n        placeholder={placeholder}\n        panelClassName={cn(\n          \"border-border\",\n          glassSurface,\n          \"shadow-[inset_0_1px_0_color-mix(in_oklab,var(--color-foreground)_12%,transparent)]\"\n        )}\n        activeClassName=\"bg-[color-mix(in_oklab,var(--color-accent)_70%,transparent)] text-accent-foreground\"\n        reduce={reduce}\n        renderOption={(option, _isActive, isSelected) => (\n          <>\n            <span className=\"flex-1 truncate\">{option.label}</span>\n            {isSelected ? <Check className=\"text-foreground\" /> : null}\n          </>\n        )}\n      />\n    </div>\n  )\n}\n\n/* IconCombobox: every option gets a leading token icon + a right-aligned check when\n   selected. */\nexport function IconCombobox({\n  className,\n  size = \"md\",\n  placeholder = \"Search...\",\n  options,\n  value,\n  defaultValue,\n  onValueChange,\n}: ComboboxProps) {\n  const state = useSingleCombobox({ options, value, defaultValue, onValueChange })\n  const reduce = useReducedMotion() ?? false\n  const { open, setOpen, selected } = state\n\n  return (\n    <div ref={state.wrapperRef} data-slot=\"styled-combobox\" className={cn(rootBase, panelWidth[size], className)}>\n      <button\n        type=\"button\"\n        role=\"combobox\"\n        aria-expanded={open}\n        aria-haspopup=\"listbox\"\n        onClick={() => setOpen(!open)}\n        className={cn(triggerBase, triggerHeight[size])}\n      >\n        <span className=\"flex min-w-0 items-center gap-2\">\n          <Hash className=\"shrink-0\" />\n          <span className={cn(\"truncate\", !selected && \"text-muted-foreground\")}>\n            {selected ? selected.label : placeholder}\n          </span>\n        </span>\n        <ChevronsUpDown className=\"opacity-70\" />\n      </button>\n      <ComboboxPanel\n        state={state}\n        size={size}\n        placeholder={placeholder}\n        activeClassName=\"bg-accent text-accent-foreground\"\n        reduce={reduce}\n        renderOption={(option, _isActive, isSelected) => (\n          <>\n            <Hash className=\"text-muted-foreground\" />\n            <span className=\"flex-1 truncate\">{option.label}</span>\n            {isSelected ? <Check className=\"text-foreground\" /> : null}\n          </>\n        )}\n      />\n    </div>\n  )\n}\n\n/* TagsCombobox: MULTIPLE selection. The selected items sit inside the trigger as removable token tag chips; clicking an option toggles it and the panel stays open. */\nexport function TagsCombobox({\n  className,\n  size = \"md\",\n  placeholder = \"Search...\",\n  options = defaultOptions,\n  value,\n  defaultValue,\n  onValueChange,\n}: ComboboxProps) {\n  const reduce = useReducedMotion() ?? false\n  const [selected, setSelected] = React.useState<string[]>(defaultValue ? [defaultValue] : [])\n  const [open, setOpen] = React.useState(false)\n  const [query, setQuery] = React.useState(\"\")\n  const [active, setActive] = React.useState(0)\n\n  const wrapperRef = React.useRef<HTMLDivElement>(null)\n  const inputRef = React.useRef<HTMLInputElement>(null)\n\n  const filtered = React.useMemo(() => filterOptions(options, query), [options, query])\n\n  React.useEffect(() => {\n    setActive((prev) => (prev >= filtered.length ? 0 : prev))\n  }, [filtered.length])\n\n  React.useEffect(() => {\n    if (!open) return\n    inputRef.current?.focus()\n    const onKey = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") setOpen(false)\n    }\n    const onPointer = (e: PointerEvent) => {\n      const node = wrapperRef.current\n      if (node && e.target instanceof Node && !node.contains(e.target)) setOpen(false)\n    }\n    window.addEventListener(\"keydown\", onKey)\n    window.addEventListener(\"pointerdown\", onPointer)\n    return () => {\n      window.removeEventListener(\"keydown\", onKey)\n      window.removeEventListener(\"pointerdown\", onPointer)\n    }\n  }, [open])\n\n  const toggle = React.useCallback(\n    (v: string) => {\n      setSelected((prev) => (prev.includes(v) ? prev.filter((x) => x !== v) : [...prev, v]))\n      onValueChange?.(v)\n    },\n    [onValueChange]\n  )\n\n  const onInputKeyDown = React.useCallback(\n    (e: React.KeyboardEvent<HTMLInputElement>) => {\n      if (e.key === \"ArrowDown\") {\n        e.preventDefault()\n        setActive((prev) => (filtered.length === 0 ? 0 : (prev + 1) % filtered.length))\n      } else if (e.key === \"ArrowUp\") {\n        e.preventDefault()\n        setActive((prev) =>\n          filtered.length === 0 ? 0 : (prev - 1 + filtered.length) % filtered.length\n        )\n      } else if (e.key === \"Enter\") {\n        e.preventDefault()\n        const item = filtered[active]\n        if (item) toggle(item.value)\n      } else if (e.key === \"Escape\") {\n        e.preventDefault()\n        setOpen(false)\n      }\n    },\n    [filtered, active, toggle]\n  )\n\n  const selectedOptions = selected\n    .map((v) => options.find((o) => o.value === v))\n    .filter((o): o is ComboboxOption => o !== undefined)\n\n  return (\n    <div ref={wrapperRef} data-slot=\"styled-combobox\" className={cn(rootBase, panelWidth[size], className)}>\n      {/* STRUCTURAL FIX (2026-07-28, the a11y triage wave).\n\n          This container used to be a <button role=\"combobox\"> with the \"remove\"\n          control of the selection chips living INSIDE IT. Because a button\n          inside a button is invalid HTML, the remove control was written as a\n          `span role=\"button\"` - and since it carried no tabIndex it was\n          COMPLETELY UNREACHABLE BY KEYBOARD: a selection could be removed with\n          the mouse but not with the keyboard.\n\n          The fix is the pattern the single-select variants in this same file\n          already use: the combobox role on a focusable CONTAINER, with the\n          remove controls as REAL <button> siblings. That leaves no nested\n          buttons and lets every control be focused on its own.\n\n          The keyboard contract is preserved: Enter/Space/ArrowDown opens the\n          list (the equivalent of the button behavior), and Backspace/Delete on\n          a chip removes the selection. */}\n      <div\n        role=\"combobox\"\n        tabIndex={0}\n        aria-expanded={open}\n        aria-haspopup=\"listbox\"\n        onClick={() => setOpen(!open)}\n        onKeyDown={(event) => {\n          if (event.key === \"Enter\" || event.key === \" \" || event.key === \"ArrowDown\") {\n            event.preventDefault()\n            setOpen(true)\n          } else if (event.key === \"Escape\") {\n            setOpen(false)\n          }\n        }}\n        className={cn(\n          triggerBase,\n          \"h-auto min-h-9 cursor-pointer flex-wrap py-1.5\",\n          size === \"sm\" && \"min-h-8\",\n          size === \"lg\" && \"min-h-10\",\n          size === \"xl\" && \"min-h-12\"\n        )}\n      >\n        <span className=\"flex flex-1 flex-wrap items-center gap-1.5\">\n          {selectedOptions.length === 0 ? (\n            <span className=\"text-muted-foreground\">{placeholder}</span>\n          ) : (\n            selectedOptions.map((option) => (\n              <span\n                key={option.value}\n                className=\"inline-flex items-center gap-1 rounded-md border border-border bg-secondary px-1.5 py-0.5 text-xs font-medium text-secondary-foreground\"\n              >\n                {option.label}\n                <button\n                  type=\"button\"\n                  aria-label={`Remove ${option.label}`}\n                  onClick={(e) => {\n                    e.stopPropagation()\n                    toggle(option.value)\n                  }}\n                  onKeyDown={(e) => e.stopPropagation()}\n                  className=\"inline-flex rounded-sm text-muted-foreground transition-colors hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none [&_svg]:size-3\"\n                >\n                  <X />\n                </button>\n              </span>\n            ))\n          )}\n        </span>\n        <ChevronsUpDown className=\"opacity-70\" />\n      </div>\n\n      <AnimatePresence>\n        {open ? (\n          <motion.div\n            role=\"listbox\"\n            aria-multiselectable=\"true\"\n            className={cn(panelBase, panelWidth[size])}\n            initial={reduce ? { opacity: 0 } : panelMotion.initial}\n            animate={reduce ? { opacity: 1 } : panelMotion.animate}\n            exit={reduce ? { opacity: 0 } : panelMotion.exit}\n            transition={reduce ? { duration: 0 } : { duration: 0.16, ease: \"easeOut\" }}\n          >\n            <div className={inputRowBase}>\n              <Search className=\"size-4 shrink-0 text-muted-foreground\" />\n              <input\n                ref={inputRef}\n                aria-autocomplete=\"list\"\n                value={query}\n                placeholder={placeholder}\n                onChange={(e) => setQuery(e.target.value)}\n                onKeyDown={onInputKeyDown}\n                className={inputBase}\n              />\n            </div>\n            <div className={listBase}>\n              {filtered.length === 0 ? (\n                <p className={emptyBase}>No results found.</p>\n              ) : (\n                filtered.map((option, i) => {\n                  const isActive = i === active\n                  const isSelected = selected.includes(option.value)\n                  return (\n                    <div\n                      key={option.value}\n                      role=\"option\"\n                      aria-selected={isSelected}\n                      onMouseEnter={() => setActive(i)}\n                      onClick={() => toggle(option.value)}\n                      className={cn(\n                        optionBase,\n                        (isActive || isSelected) && \"bg-accent text-accent-foreground\"\n                      )}\n                    >\n                      <span\n                        className={cn(\n                          \"flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-border\",\n                          isSelected && \"border-transparent bg-primary text-primary-foreground\"\n                        )}\n                      >\n                        {isSelected ? <Check className=\"size-3\" /> : null}\n                      </span>\n                      <span className=\"flex-1 truncate\">{option.label}</span>\n                    </div>\n                  )\n                })\n              )}\n            </div>\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  )\n}\n\n/* InlineCombobox: the search <input> ITSELF is the trigger. Results drop below as you type; on selection the input fills with the chosen label and the panel closes. */\nexport function InlineCombobox({\n  className,\n  size = \"md\",\n  placeholder = \"Search...\",\n  options = defaultOptions,\n  value,\n  defaultValue,\n  onValueChange,\n}: ComboboxProps) {\n  const reduce = useReducedMotion() ?? false\n  const isControlled = value !== undefined\n  const [internal, setInternal] = React.useState<string | undefined>(defaultValue)\n  const current = isControlled ? value : internal\n\n  const selected = React.useMemo(\n    () => (current === undefined ? undefined : options.find((o) => o.value === current)),\n    [options, current]\n  )\n\n  const [open, setOpen] = React.useState(false)\n  const [editing, setEditing] = React.useState(false)\n  const [query, setQuery] = React.useState(\"\")\n  const [active, setActive] = React.useState(0)\n\n  const wrapperRef = React.useRef<HTMLDivElement>(null)\n\n  /* Filter as you type; show the whole list only on focus (not while editing). */\n  const filtered = React.useMemo(\n    () => (editing ? filterOptions(options, query) : options),\n    [editing, options, query]\n  )\n\n  React.useEffect(() => {\n    setActive((prev) => (prev >= filtered.length ? 0 : prev))\n  }, [filtered.length])\n\n  const close = React.useCallback(() => {\n    setOpen(false)\n    setEditing(false)\n    setQuery(\"\")\n    setActive(0)\n  }, [])\n\n  React.useEffect(() => {\n    if (!open) return\n    const onKey = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") close()\n    }\n    const onPointer = (e: PointerEvent) => {\n      const node = wrapperRef.current\n      if (node && e.target instanceof Node && !node.contains(e.target)) close()\n    }\n    window.addEventListener(\"keydown\", onKey)\n    window.addEventListener(\"pointerdown\", onPointer)\n    return () => {\n      window.removeEventListener(\"keydown\", onKey)\n      window.removeEventListener(\"pointerdown\", onPointer)\n    }\n  }, [open, close])\n\n  const commit = React.useCallback(\n    (next: string) => {\n      if (!isControlled) setInternal(next)\n      onValueChange?.(next)\n      close()\n    },\n    [isControlled, onValueChange, close]\n  )\n\n  const onInputKeyDown = React.useCallback(\n    (e: React.KeyboardEvent<HTMLInputElement>) => {\n      if (e.key === \"ArrowDown\") {\n        e.preventDefault()\n        setOpen(true)\n        setActive((prev) => (filtered.length === 0 ? 0 : (prev + 1) % filtered.length))\n      } else if (e.key === \"ArrowUp\") {\n        e.preventDefault()\n        setActive((prev) =>\n          filtered.length === 0 ? 0 : (prev - 1 + filtered.length) % filtered.length\n        )\n      } else if (e.key === \"Enter\") {\n        e.preventDefault()\n        const item = filtered[active]\n        if (item) commit(item.value)\n      } else if (e.key === \"Escape\") {\n        e.preventDefault()\n        close()\n      }\n    },\n    [filtered, active, commit, close]\n  )\n\n  const inputValue = editing ? query : selected ? nodeText(selected.label) : \"\"\n\n  return (\n    <div ref={wrapperRef} data-slot=\"styled-combobox\" className={cn(rootBase, panelWidth[size], className)}>\n      <div className={cn(inputRowBase, \"rounded-lg border border-field-border px-3\", triggerHeight[size])}>\n        <Search className=\"size-4 shrink-0 text-muted-foreground\" />\n        <input\n          role=\"combobox\"\n          aria-expanded={open}\n          aria-autocomplete=\"list\"\n          value={inputValue}\n          placeholder={placeholder}\n          onFocus={() => {\n            setOpen(true)\n            setEditing(false)\n          }}\n          onChange={(e) => {\n            setEditing(true)\n            setOpen(true)\n            setQuery(e.target.value)\n          }}\n          onKeyDown={onInputKeyDown}\n          className={cn(inputBase, \"text-foreground\")}\n        />\n      </div>\n\n      <AnimatePresence>\n        {open ? (\n          <motion.div\n            role=\"listbox\"\n            className={cn(panelBase, panelWidth[size])}\n            initial={reduce ? { opacity: 0 } : panelMotion.initial}\n            animate={reduce ? { opacity: 1 } : panelMotion.animate}\n            exit={reduce ? { opacity: 0 } : panelMotion.exit}\n            transition={reduce ? { duration: 0 } : { duration: 0.16, ease: \"easeOut\" }}\n          >\n            <div className={listBase}>\n              {filtered.length === 0 ? (\n                <p className={emptyBase}>No results found.</p>\n              ) : (\n                filtered.map((option, i) => {\n                  const isActive = i === active\n                  const isSelected = option.value === current\n                  return (\n                    <div\n                      key={option.value}\n                      role=\"option\"\n                      aria-selected={isSelected}\n                      onMouseEnter={() => setActive(i)}\n                      onClick={() => commit(option.value)}\n                      className={cn(\n                        optionBase,\n                        (isActive || isSelected) && \"bg-accent text-accent-foreground\"\n                      )}\n                    >\n                      <span className=\"flex-1 truncate\">{option.label}</span>\n                      {isSelected ? <Check className=\"text-foreground\" /> : null}\n                    </div>\n                  )\n                })\n              )}\n            </div>\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ui/combobox-styled.tsx"
    }
  ],
  "categories": [
    "styled",
    "combobox"
  ],
  "type": "registry:component"
}