# Shadcn Combobox

> Free shadcn/ui combobox for React — the searchable select / autocomplete built from Popover + Command. A drop-in component plus raw composition: grouped options, icons, async search, create-new, and a responsive Drawer. The examples official shadcn never shipped.

Source: https://designrevision.com/components/combobox

The **Shadcn Combobox** is the searchable select — an autocomplete built from a [Popover](/components/popover) and a [Command](/components/command) menu. Officially shadcn ships it as a *composition pattern* rather than a component; we give you **both** — a drop-in `<Combobox>` for the common case and the raw Popover + Command composition for everything custom: grouped options, icons, async search, create-new, and a responsive [Drawer](/components/drawer) on mobile.

## Component or composition?

There's no canonical `Combobox` primitive in shadcn — it's Popover + Command wired together. That's flexible but repetitive, so we provide a small reusable `<Combobox>` that covers the 80% case:

```tsx
<Combobox
  options={frameworks}
  value={value}
  onValueChange={setValue}
  placeholder="Select framework…"
/>
```

Reach for the **raw composition** (shown in every example below the first) when you need a custom trigger, grouped headings, icons, async loading, or a create-new action. Both render the same UI — pick the level of control you want.

## Combobox vs Autocomplete vs Select

These names overlap, so to be concrete:

- **[Select](/components/select)** — a short, static list; no typing. Best when there are only a handful of options.
- **Combobox / Autocomplete** — a searchable list; type to filter, then pick. shadcn has no separate "Autocomplete" — the Combobox **is** it. Best for long lists (countries, frameworks, users).
- **[Multi-Select](/components/multi-select)** — a Combobox that keeps several values as removable badges.

## Matching the popover to the trigger width

The one detail every combobox needs: the dropdown should be as wide as the button. Radix exposes the trigger width to the popover as a CSS variable, so set it on `PopoverContent`:

```tsx
<PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0">
```

Our `<Combobox>` wrapper applies this automatically; the raw examples use a fixed width for clarity.

## Grouped options

Wrap sets of items in `CommandGroup` with a `heading` to organise a long list into sections — the Grouped options example splits produce into Fruits and Vegetables. cmdk keeps the headings while filtering.

## Icons & status

A combobox isn't just text — render an icon in both the trigger and each item for status pickers, country flags, or user avatars. The Icons & status example is the issue-tracker pattern (Backlog / In Progress / Done / Canceled).

## Responsive (Popover + Drawer)

On a phone, a popover anchored to a button is cramped. The pattern shadcn documents is a **Popover on desktop and a [Drawer](/components/drawer) on mobile**: detect the viewport with a `useMediaQuery` hook and render the same `Command` list in whichever container fits. The Responsive example is the full implementation.

## Async / server-side search

For options that live on a server, set `shouldFilter={false}` so cmdk stops filtering locally, drive `CommandInput` as a controlled value, debounce a request, and render the results you receive — with a loading spinner in between. The Async / server search example simulates the round-trip end to end.

## Create-new option

When the search matches nothing, let the user **add** it: offer a "Create '<query>'" item that appends a new option and selects it. It's the tag-input pattern, and the Create new option example shows it with a `shouldFilter={false}` list you control.

## Accessibility

The trigger is a `Button` with `role="combobox"` and `aria-expanded`, so it's announced correctly; cmdk handles arrow-key navigation, type-ahead, and Enter to select inside the list. Always give the popover a searchable `CommandInput` and a `CommandEmpty` fallback so keyboard and screen-reader users get feedback when nothing matches.

## Theming

The combobox reads your design tokens — `--popover`, `--accent` (the highlighted item), `--input` (the trigger border) — so it follows your theme, including dark mode, with no overrides.

## Installation

### combobox

**Install:**

```bash
npx shadcn@latest add @designrevision/combobox
```

**Dependencies:** lucide-react, utils, popover, command, button

**Props**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| options * | { value: string; label: string; disabled?: boolean }[] | — | The selectable options. label is shown and searched; value is returned on select. |
| value | string | — | Controlled selected value. Selecting the active option again clears it. |
| onValueChange | (value: string) => void | — | Fires with the new value (empty string when cleared). |
| placeholder | string | "Select an option…" | Trigger text shown when nothing is selected. |
| searchPlaceholder | string | "Search…" | Placeholder for the search input. |
| emptyText | string | "No results found." | Message shown when the search matches nothing. |

`*` required.

**Usage & accessibility:** A reusable single-select combobox wrapping Popover + Command + Button (cmdk-powered search), with the popover width matched to the trigger via --radix-popover-trigger-width. For grouped options, icons, async search, or create-new, compose Popover + Command directly (see the examples); for multiple selection use the Multi-Select component.

```tsx
// components/ui/combobox.tsx
"use client"

import * as React from "react"
import { CheckIcon, ChevronsUpDownIcon } from "lucide-react"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
} from "@/components/ui/command"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"

export interface ComboboxOption {
  value: string
  label: string
  disabled?: boolean
}

export interface ComboboxProps {
  options: ComboboxOption[]
  value?: string
  onValueChange?: (value: string) => void
  placeholder?: string
  searchPlaceholder?: string
  emptyText?: string
  className?: string
  disabled?: boolean
  id?: string
}

function Combobox({
  options,
  value,
  onValueChange,
  placeholder = "Select an option…",
  searchPlaceholder = "Search…",
  emptyText = "No results found.",
  className,
  disabled,
  id,
}: ComboboxProps) {
  const [open, setOpen] = React.useState(false)
  const selected = options.find((option) => option.value === value)

  return (
    <Popover open={open} onOpenChange={setOpen}>
      <PopoverTrigger asChild>
        <Button
          id={id}
          type="button"
          variant="outline"
          role="combobox"
          aria-expanded={open}
          disabled={disabled}
          className={cn("w-full justify-between font-normal", className)}
        >
          <span
            className={cn("truncate", !selected && "text-muted-foreground")}
          >
            {selected ? selected.label : placeholder}
          </span>
          <ChevronsUpDownIcon className="ml-2 size-4 shrink-0 opacity-50" />
        </Button>
      </PopoverTrigger>
      <PopoverContent
        className="w-[var(--radix-popover-trigger-width)] p-0"
        align="start"
      >
        <Command>
          <CommandInput placeholder={searchPlaceholder} />
          <CommandList>
            <CommandEmpty>{emptyText}</CommandEmpty>
            <CommandGroup>
              {options.map((option) => (
                <CommandItem
                  key={option.value}
                  value={option.label}
                  disabled={option.disabled}
                  onSelect={() => {
                    onValueChange?.(option.value === value ? "" : option.value)
                    setOpen(false)
                  }}
                >
                  <CheckIcon
                    className={cn(
                      "mr-2 size-4",
                      option.value === value ? "opacity-100" : "opacity-0"
                    )}
                  />
                  {option.label}
                </CommandItem>
              ))}
            </CommandGroup>
          </CommandList>
        </Command>
      </PopoverContent>
    </Popover>
  )
}

export { Combobox }
```

## Examples

### Combobox

The drop-in `<Combobox>` — a searchable single-select built from an `options` array with a controlled value.

**Install:**

```bash
npx shadcn@latest add @designrevision/combobox-demo-01
```

**Dependencies:** @designrevision/combobox

```tsx
// components/ui/combobox-demo-01.tsx
"use client"

import * as React from "react"

import { Combobox } from "@/components/ui/combobox"

const frameworks = [
  { value: "next.js", label: "Next.js" },
  { value: "sveltekit", label: "SvelteKit" },
  { value: "nuxt.js", label: "Nuxt.js" },
  { value: "remix", label: "Remix" },
  { value: "astro", label: "Astro" },
]

export default function ComboboxDemo01() {
  const [value, setValue] = React.useState("")

  return (
    <div className="w-[240px]">
      <Combobox
        options={frameworks}
        value={value}
        onValueChange={setValue}
        placeholder="Select framework…"
        searchPlaceholder="Search framework…"
        emptyText="No framework found."
      />
    </div>
  )
}
```

---

### Raw composition

The canonical shadcn pattern — [Popover](/components/popover) + [Command](/components/command) composed by hand for full control over the trigger and items.

**Install:**

```bash
npx shadcn@latest add @designrevision/combobox-composition-01
```

**Dependencies:** lucide-react, popover, command, button

```tsx
// components/ui/combobox-composition-01.tsx
"use client"

import * as React from "react"
import { CheckIcon, ChevronsUpDownIcon } from "lucide-react"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
} from "@/components/ui/command"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"

const frameworks = [
  { value: "next.js", label: "Next.js" },
  { value: "sveltekit", label: "SvelteKit" },
  { value: "nuxt.js", label: "Nuxt.js" },
  { value: "remix", label: "Remix" },
  { value: "astro", label: "Astro" },
]

export default function ComboboxComposition01() {
  const [open, setOpen] = React.useState(false)
  const [value, setValue] = React.useState("")

  return (
    <Popover open={open} onOpenChange={setOpen}>
      <PopoverTrigger asChild>
        <Button
          variant="outline"
          role="combobox"
          aria-expanded={open}
          className="w-[240px] justify-between font-normal"
        >
          {value
            ? frameworks.find((f) => f.value === value)?.label
            : "Select framework…"}
          <ChevronsUpDownIcon className="ml-2 size-4 shrink-0 opacity-50" />
        </Button>
      </PopoverTrigger>
      <PopoverContent className="w-[240px] p-0" align="start">
        <Command>
          <CommandInput placeholder="Search framework…" />
          <CommandList>
            <CommandEmpty>No framework found.</CommandEmpty>
            <CommandGroup>
              {frameworks.map((framework) => (
                <CommandItem
                  key={framework.value}
                  value={framework.label}
                  onSelect={() => {
                    setValue(framework.value === value ? "" : framework.value)
                    setOpen(false)
                  }}
                >
                  <CheckIcon
                    className={cn(
                      "mr-2 size-4",
                      value === framework.value ? "opacity-100" : "opacity-0"
                    )}
                  />
                  {framework.label}
                </CommandItem>
              ))}
            </CommandGroup>
          </CommandList>
        </Command>
      </PopoverContent>
    </Popover>
  )
}
```

---

### In a form

A labelled, controlled combobox inside a form that reports the selected value on submit.

**Install:**

```bash
npx shadcn@latest add @designrevision/combobox-form-01
```

**Dependencies:** @designrevision/combobox, button, label

```tsx
// components/ui/combobox-form-01.tsx
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { Combobox } from "@/components/ui/combobox"
import { Label } from "@/components/ui/label"

const languages = [
  { value: "en", label: "English" },
  { value: "fr", label: "French" },
  { value: "de", label: "German" },
  { value: "es", label: "Spanish" },
  { value: "pt", label: "Portuguese" },
]

export default function ComboboxForm01() {
  const [language, setLanguage] = React.useState("")
  const [submitted, setSubmitted] = React.useState<string | null>(null)

  return (
    <form
      onSubmit={(event) => {
        event.preventDefault()
        setSubmitted(
          languages.find((l) => l.value === language)?.label ?? "nothing"
        )
      }}
      className="grid w-[260px] gap-3"
    >
      <div className="grid gap-2">
        <Label htmlFor="language">Language</Label>
        <Combobox
          id="language"
          options={languages}
          value={language}
          onValueChange={setLanguage}
          placeholder="Select language…"
          searchPlaceholder="Search language…"
          emptyText="No language found."
        />
        <p className="text-sm text-muted-foreground">
          This is the language used across your dashboard.
        </p>
      </div>
      <Button type="submit" className="w-fit">
        Save
      </Button>
      {submitted && (
        <p className="text-sm">
          Saved: <span className="font-medium">{submitted}</span>
        </p>
      )}
    </form>
  )
}
```

---

### Grouped options

Options organised under category headings with `CommandGroup`.

**Install:**

```bash
npx shadcn@latest add @designrevision/combobox-groups-01
```

**Dependencies:** lucide-react, popover, command, button

```tsx
// components/ui/combobox-groups-01.tsx
"use client"

import * as React from "react"
import { CheckIcon, ChevronsUpDownIcon } from "lucide-react"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
} from "@/components/ui/command"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"

const groups = [
  {
    label: "Fruits",
    items: [
      { value: "apple", label: "Apple" },
      { value: "banana", label: "Banana" },
      { value: "orange", label: "Orange" },
    ],
  },
  {
    label: "Vegetables",
    items: [
      { value: "carrot", label: "Carrot" },
      { value: "potato", label: "Potato" },
      { value: "spinach", label: "Spinach" },
    ],
  },
]

const allItems = groups.flatMap((group) => group.items)

export default function ComboboxGroups01() {
  const [open, setOpen] = React.useState(false)
  const [value, setValue] = React.useState("")

  return (
    <Popover open={open} onOpenChange={setOpen}>
      <PopoverTrigger asChild>
        <Button
          variant="outline"
          role="combobox"
          aria-expanded={open}
          className="w-[240px] justify-between font-normal"
        >
          {value
            ? allItems.find((item) => item.value === value)?.label
            : "Select produce…"}
          <ChevronsUpDownIcon className="ml-2 size-4 shrink-0 opacity-50" />
        </Button>
      </PopoverTrigger>
      <PopoverContent className="w-[240px] p-0" align="start">
        <Command>
          <CommandInput placeholder="Search produce…" />
          <CommandList>
            <CommandEmpty>No produce found.</CommandEmpty>
            {groups.map((group) => (
              <CommandGroup key={group.label} heading={group.label}>
                {group.items.map((item) => (
                  <CommandItem
                    key={item.value}
                    value={item.label}
                    onSelect={() => {
                      setValue(item.value === value ? "" : item.value)
                      setOpen(false)
                    }}
                  >
                    <CheckIcon
                      className={cn(
                        "mr-2 size-4",
                        value === item.value ? "opacity-100" : "opacity-0"
                      )}
                    />
                    {item.label}
                  </CommandItem>
                ))}
              </CommandGroup>
            ))}
          </CommandList>
        </Command>
      </PopoverContent>
    </Popover>
  )
}
```

---

### Icons & status

A status picker with an icon in both the trigger and each item — the issue-tracker pattern.

**Install:**

```bash
npx shadcn@latest add @designrevision/combobox-icons-01
```

**Dependencies:** lucide-react, popover, command, button

```tsx
// components/ui/combobox-icons-01.tsx
"use client"

import * as React from "react"
import {
  CheckIcon,
  ChevronsUpDownIcon,
  CircleCheckIcon,
  CircleDotIcon,
  CircleIcon,
  CircleXIcon,
} from "lucide-react"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
} from "@/components/ui/command"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"

const statuses = [
  { value: "backlog", label: "Backlog", icon: CircleIcon },
  { value: "in-progress", label: "In Progress", icon: CircleDotIcon },
  { value: "done", label: "Done", icon: CircleCheckIcon },
  { value: "canceled", label: "Canceled", icon: CircleXIcon },
]

export default function ComboboxIcons01() {
  const [open, setOpen] = React.useState(false)
  const [value, setValue] = React.useState("")
  const selected = statuses.find((status) => status.value === value)

  return (
    <Popover open={open} onOpenChange={setOpen}>
      <PopoverTrigger asChild>
        <Button
          variant="outline"
          role="combobox"
          aria-expanded={open}
          className="w-[220px] justify-between font-normal"
        >
          <span className="flex min-w-0 items-center gap-2">
            {selected && <selected.icon className="size-4 shrink-0" />}
            <span className={cn("truncate", !selected && "text-muted-foreground")}>
              {selected ? selected.label : "Select status…"}
            </span>
          </span>
          <ChevronsUpDownIcon className="ml-2 size-4 shrink-0 opacity-50" />
        </Button>
      </PopoverTrigger>
      <PopoverContent className="w-[220px] p-0" align="start">
        <Command>
          <CommandInput placeholder="Change status…" />
          <CommandList>
            <CommandEmpty>No status found.</CommandEmpty>
            <CommandGroup>
              {statuses.map((status) => (
                <CommandItem
                  key={status.value}
                  value={status.label}
                  onSelect={() => {
                    setValue(status.value === value ? "" : status.value)
                    setOpen(false)
                  }}
                >
                  <status.icon className="size-4" />
                  <span>{status.label}</span>
                  <CheckIcon
                    className={cn(
                      "ml-auto size-4",
                      value === status.value ? "opacity-100" : "opacity-0"
                    )}
                  />
                </CommandItem>
              ))}
            </CommandGroup>
          </CommandList>
        </Command>
      </PopoverContent>
    </Popover>
  )
}
```

---

### Responsive (Popover + Drawer)

A Popover on desktop and a [Drawer](/components/drawer) on mobile via `useMediaQuery`, sharing one Command list.

**Install:**

```bash
npx shadcn@latest add @designrevision/combobox-responsive-01
```

**Dependencies:** popover, command, @designrevision/drawer, button

```tsx
// components/ui/combobox-responsive-01.tsx
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
} from "@/components/ui/command"
import {
  Drawer,
  DrawerContent,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"

type Status = { value: string; label: string }

const statuses: Status[] = [
  { value: "backlog", label: "Backlog" },
  { value: "todo", label: "Todo" },
  { value: "in-progress", label: "In Progress" },
  { value: "done", label: "Done" },
  { value: "canceled", label: "Canceled" },
]

function useMediaQuery(query: string) {
  const [matches, setMatches] = React.useState(false)

  React.useEffect(() => {
    const result = window.matchMedia(query)
    const onChange = () => setMatches(result.matches)
    onChange()
    result.addEventListener("change", onChange)
    return () => result.removeEventListener("change", onChange)
  }, [query])

  return matches
}

function StatusList({
  setOpen,
  setSelected,
}: {
  setOpen: (open: boolean) => void
  setSelected: (status: Status | null) => void
}) {
  return (
    <Command>
      <CommandInput placeholder="Filter status…" />
      <CommandList>
        <CommandEmpty>No results found.</CommandEmpty>
        <CommandGroup>
          {statuses.map((status) => (
            <CommandItem
              key={status.value}
              value={status.value}
              onSelect={(value) => {
                setSelected(statuses.find((s) => s.value === value) ?? null)
                setOpen(false)
              }}
            >
              {status.label}
            </CommandItem>
          ))}
        </CommandGroup>
      </CommandList>
    </Command>
  )
}

export default function ComboboxResponsive01() {
  const [open, setOpen] = React.useState(false)
  const [selected, setSelected] = React.useState<Status | null>(null)
  const isDesktop = useMediaQuery("(min-width: 768px)")
  const label = selected ? selected.label : "+ Set status"

  if (isDesktop) {
    return (
      <Popover open={open} onOpenChange={setOpen}>
        <PopoverTrigger asChild>
          <Button variant="outline" className="w-[200px] justify-start font-normal">
            {label}
          </Button>
        </PopoverTrigger>
        <PopoverContent className="w-[200px] p-0" align="start">
          <StatusList setOpen={setOpen} setSelected={setSelected} />
        </PopoverContent>
      </Popover>
    )
  }

  return (
    <Drawer open={open} onOpenChange={setOpen}>
      <DrawerTrigger asChild>
        <Button variant="outline" className="w-[200px] justify-start font-normal">
          {label}
        </Button>
      </DrawerTrigger>
      <DrawerContent>
        <DrawerHeader className="sr-only">
          <DrawerTitle>Set status</DrawerTitle>
        </DrawerHeader>
        <div className="mt-4 border-t">
          <StatusList setOpen={setOpen} setSelected={setSelected} />
        </div>
      </DrawerContent>
    </Drawer>
  )
}
```

---

### Async / server search

Server-side search with `shouldFilter={false}`, a debounced request, and a loading spinner.

**Install:**

```bash
npx shadcn@latest add @designrevision/combobox-async-01
```

**Dependencies:** lucide-react, popover, command, button

```tsx
// components/ui/combobox-async-01.tsx
"use client"

import * as React from "react"
import { ChevronsUpDownIcon, Loader2Icon } from "lucide-react"

import { Button } from "@/components/ui/button"
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
} from "@/components/ui/command"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"

const ALL = [
  "Next.js",
  "React",
  "Vue",
  "Svelte",
  "Angular",
  "Solid",
  "Astro",
  "Remix",
  "Nuxt",
  "Qwik",
]

export default function ComboboxAsync01() {
  const [open, setOpen] = React.useState(false)
  const [value, setValue] = React.useState("")
  const [query, setQuery] = React.useState("")
  const [results, setResults] = React.useState<string[]>([])
  const [loading, setLoading] = React.useState(false)

  React.useEffect(() => {
    if (!query) {
      setResults([])
      setLoading(false)
      return
    }
    // Simulate a debounced server request.
    setLoading(true)
    const id = setTimeout(() => {
      setResults(ALL.filter((f) => f.toLowerCase().includes(query.toLowerCase())))
      setLoading(false)
    }, 500)
    return () => clearTimeout(id)
  }, [query])

  return (
    <Popover open={open} onOpenChange={setOpen}>
      <PopoverTrigger asChild>
        <Button
          variant="outline"
          role="combobox"
          aria-expanded={open}
          className="w-[240px] justify-between font-normal"
        >
          <span className={value ? "" : "text-muted-foreground"}>
            {value || "Search frameworks…"}
          </span>
          <ChevronsUpDownIcon className="ml-2 size-4 shrink-0 opacity-50" />
        </Button>
      </PopoverTrigger>
      <PopoverContent className="w-[240px] p-0" align="start">
        <Command shouldFilter={false}>
          <CommandInput
            value={query}
            onValueChange={setQuery}
            placeholder="Type to search (simulated)…"
          />
          <CommandList>
            {loading && (
              <div className="flex items-center justify-center gap-2 py-6 text-sm text-muted-foreground">
                <Loader2Icon className="size-4 animate-spin" />
                Searching…
              </div>
            )}
            {!loading && query && results.length === 0 && (
              <CommandEmpty>No results found.</CommandEmpty>
            )}
            {!loading && results.length > 0 && (
              <CommandGroup>
                {results.map((framework) => (
                  <CommandItem
                    key={framework}
                    value={framework}
                    onSelect={(current) => {
                      setValue(current === value ? "" : framework)
                      setOpen(false)
                    }}
                  >
                    {framework}
                  </CommandItem>
                ))}
              </CommandGroup>
            )}
          </CommandList>
        </Command>
      </PopoverContent>
    </Popover>
  )
}
```

---

### Create new option

A tag picker that offers **Create "<query>"** when the search matches nothing — add on the fly.

**Install:**

```bash
npx shadcn@latest add @designrevision/combobox-create-01
```

**Dependencies:** lucide-react, popover, command, button

```tsx
// components/ui/combobox-create-01.tsx
"use client"

import * as React from "react"
import { CheckIcon, ChevronsUpDownIcon, PlusIcon } from "lucide-react"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
} from "@/components/ui/command"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"

export default function ComboboxCreate01() {
  const [open, setOpen] = React.useState(false)
  const [query, setQuery] = React.useState("")
  const [value, setValue] = React.useState("")
  const [options, setOptions] = React.useState([
    { value: "design", label: "Design" },
    { value: "engineering", label: "Engineering" },
    { value: "marketing", label: "Marketing" },
  ])

  const filtered = options.filter((option) =>
    option.label.toLowerCase().includes(query.toLowerCase())
  )
  const exists = options.some(
    (option) => option.label.toLowerCase() === query.trim().toLowerCase()
  )
  const selected = options.find((option) => option.value === value)

  const createOption = () => {
    const label = query.trim()
    if (!label) return
    const next = label.toLowerCase().replace(/\s+/g, "-")
    setOptions((prev) => [...prev, { value: next, label }])
    setValue(next)
    setQuery("")
    setOpen(false)
  }

  return (
    <Popover open={open} onOpenChange={setOpen}>
      <PopoverTrigger asChild>
        <Button
          variant="outline"
          role="combobox"
          aria-expanded={open}
          className="w-[240px] justify-between font-normal"
        >
          <span className={selected ? "" : "text-muted-foreground"}>
            {selected ? selected.label : "Select or create tag…"}
          </span>
          <ChevronsUpDownIcon className="ml-2 size-4 shrink-0 opacity-50" />
        </Button>
      </PopoverTrigger>
      <PopoverContent className="w-[240px] p-0" align="start">
        <Command shouldFilter={false}>
          <CommandInput
            value={query}
            onValueChange={setQuery}
            placeholder="Search or add a tag…"
          />
          <CommandList>
            {filtered.length === 0 && !query.trim() && (
              <CommandEmpty>No tags found.</CommandEmpty>
            )}
            <CommandGroup>
              {filtered.map((option) => (
                <CommandItem
                  key={option.value}
                  value={option.value}
                  onSelect={() => {
                    setValue(option.value === value ? "" : option.value)
                    setOpen(false)
                  }}
                >
                  <CheckIcon
                    className={cn(
                      "mr-2 size-4",
                      value === option.value ? "opacity-100" : "opacity-0"
                    )}
                  />
                  {option.label}
                </CommandItem>
              ))}
              {query.trim() && !exists && (
                <CommandItem value="__create__" onSelect={createOption}>
                  <PlusIcon className="mr-2 size-4" />
                  Create “{query.trim()}”
                </CommandItem>
              )}
            </CommandGroup>
          </CommandList>
        </Command>
      </PopoverContent>
    </Popover>
  )
}
```
