{
	"$schema": "https://shadcn-svelte.com/schema/registry-item.json",
	"name": "combobox",
	"title": "Combobox",
	"type": "registry:ui",
	"description": "A searchable single-select with grouped options, custom values, rich snippets, and a native datalist fallback.",
	"dependencies": [
		"@lucide/svelte@^1.28.0"
	],
	"registryDependencies": [
		"button",
		"command",
		"input",
		"popover"
	],
	"files": [
		{
			"content": "<script lang=\"ts\" generics=\"T extends ComboboxOption\">\n\timport CheckIcon from \"@lucide/svelte/icons/check\";\n\timport ChevronsUpDownIcon from \"@lucide/svelte/icons/chevrons-up-down\";\n\timport PlusIcon from \"@lucide/svelte/icons/plus\";\n\timport { onMount, type Snippet, tick } from \"svelte\";\n\n\timport { Button, type ButtonVariant } from \"$UI$/button/index.js\";\n\timport * as Command from \"$UI$/command/index.js\";\n\timport { Input } from \"$UI$/input/index.js\";\n\timport * as Popover from \"$UI$/popover/index.js\";\n\timport { cn } from \"$UTILS$.js\";\n\n\timport type { ComboboxOption } from \"./combobox.js\";\n\n\timport { customComboboxValue, groupComboboxOptions } from \"./combobox.js\";\n\n\tlet {\n\t\tallowCustom = false,\n\t\tclass: className,\n\t\tclearLabel,\n\t\tcustomLabel = (text: string) => `Use “${text}”`,\n\t\tdisabled = false,\n\t\temptyLabel = \"No option found.\",\n\t\tid,\n\t\tinvalid = false,\n\t\titem,\n\t\tmaxLength,\n\t\tname,\n\t\tonSelect,\n\t\topen = $bindable(false),\n\t\toptions,\n\t\tplaceholder = \"Select an option\",\n\t\trequired = false,\n\t\tsearchPlaceholder = \"Search...\",\n\t\tselected,\n\t\tvalue = $bindable(\"\"),\n\t\tvariant = \"secondary\",\n\t}: {\n\t\tallowCustom?: boolean;\n\t\tclass?: string;\n\t\tclearLabel?: string;\n\t\tcustomLabel?: (text: string) => string;\n\t\tdisabled?: boolean;\n\t\temptyLabel?: string;\n\t\tid: string;\n\t\tinvalid?: boolean;\n\t\titem?: Snippet<[T, boolean]>;\n\t\tmaxLength?: number;\n\t\tname: string;\n\t\tonSelect?: (value: string, option: null | T) => Promise<void> | void;\n\t\topen?: boolean;\n\t\toptions: readonly T[];\n\t\tplaceholder?: string;\n\t\trequired?: boolean;\n\t\tsearchPlaceholder?: string;\n\t\tselected?: Snippet<[T]>;\n\t\tvalue?: string;\n\t\tvariant?: ButtonVariant;\n\t} = $props();\n\n\tlet enhanced = $state(false);\n\tlet placed = $state(false);\n\tlet search = $state(\"\");\n\tlet searchRef = $state<HTMLInputElement | null>(null);\n\tlet triggerRef = $state<HTMLButtonElement | null>(null);\n\n\tonMount(() => {\n\t\tenhanced = true;\n\t});\n\n\tconst selectedOption = $derived(options.find((option) => option.value === value));\n\tconst customValue = $derived(customComboboxValue(options, search, allowCustom));\n\tconst groups = $derived(groupComboboxOptions(options));\n\tconst listId = $derived(`${id}-options`);\n\n\tasync function choose(nextValue: string, option: null | T): Promise<void> {\n\t\tvalue = nextValue;\n\t\tsearch = \"\";\n\t\topen = false;\n\t\tawait onSelect?.(nextValue, option);\n\t\tawait tick();\n\t\ttriggerRef?.focus();\n\t}\n\n\tfunction revealOptions(): void {\n\t\trequestAnimationFrame(() => {\n\t\t\tplaced = true;\n\t\t});\n\t}\n</script>\n\n{#if enhanced}\n\t<input type=\"hidden\" {name} {value} />\n\t<Popover.Root bind:open>\n\t\t<Popover.Trigger bind:ref={triggerRef}>\n\t\t\t{#snippet child({ props })}\n\t\t\t\t<Button\n\t\t\t\t\t{...props}\n\t\t\t\t\t{id}\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t{variant}\n\t\t\t\t\tclass={cn(\"w-full justify-between font-normal\", className)}\n\t\t\t\t\trole=\"combobox\"\n\t\t\t\t\taria-expanded={open}\n\t\t\t\t\taria-required={required}\n\t\t\t\t\taria-invalid={invalid}\n\t\t\t\t\taria-controls={listId}\n\t\t\t\t\t{disabled}\n\t\t\t\t>\n\t\t\t\t\t<span class={cn(\"min-w-0 flex-1 truncate text-start\", !value && \"text-muted-foreground\")}>\n\t\t\t\t\t\t{#if selectedOption}\n\t\t\t\t\t\t\t{#if selected}\n\t\t\t\t\t\t\t\t{@render selected(selectedOption)}\n\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\t{selectedOption.label}\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t{value || placeholder}\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t</span>\n\t\t\t\t\t<ChevronsUpDownIcon class=\"opacity-50\" />\n\t\t\t\t</Button>\n\t\t\t{/snippet}\n\t\t</Popover.Trigger>\n\t\t<Popover.Content\n\t\t\tclass=\"w-(--bits-popover-anchor-width) p-0\"\n\t\t\talign=\"start\"\n\t\t\tonOpenAutoFocus={(event) => {\n\t\t\t\tevent.preventDefault();\n\t\t\t\tsearchRef?.focus({ preventScroll: true });\n\t\t\t\trevealOptions();\n\t\t\t}}\n\t\t\tonCloseAutoFocus={() => {\n\t\t\t\tplaced = false;\n\t\t\t\tsearch = \"\";\n\t\t\t}}\n\t\t>\n\t\t\t<Command.Root>\n\t\t\t\t<Command.Input bind:ref={searchRef} bind:value={search} placeholder={searchPlaceholder} />\n\t\t\t\t<Command.List id={listId}>\n\t\t\t\t\t{#if placed}\n\t\t\t\t\t\t<Command.Empty>{emptyLabel}</Command.Empty>\n\t\t\t\t\t\t{#if clearLabel}\n\t\t\t\t\t\t\t<Command.Group>\n\t\t\t\t\t\t\t\t<Command.Item value={clearLabel} onSelect={() => choose(\"\", null)}>\n\t\t\t\t\t\t\t\t\t<span class=\"min-w-0 flex-1 truncate text-muted-foreground\">{clearLabel}</span>\n\t\t\t\t\t\t\t\t\t{#if value === \"\"}<CheckIcon />{/if}\n\t\t\t\t\t\t\t\t</Command.Item>\n\t\t\t\t\t\t\t</Command.Group>\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t{#each groups as group (group.label)}\n\t\t\t\t\t\t\t<Command.Group heading={group.label || undefined}>\n\t\t\t\t\t\t\t\t{#each group.options as option (option.value)}\n\t\t\t\t\t\t\t\t\t<Command.Item\n\t\t\t\t\t\t\t\t\t\tvalue={option.keywords ?? `${option.label} ${option.value}`}\n\t\t\t\t\t\t\t\t\t\tdisabled={option.disabled}\n\t\t\t\t\t\t\t\t\t\tonSelect={() => choose(option.value, option)}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{#if item}\n\t\t\t\t\t\t\t\t\t\t\t{@render item(option, value === option.value)}\n\t\t\t\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\t\t\t\t<span class=\"min-w-0 flex-1 truncate\">{option.label}</span>\n\t\t\t\t\t\t\t\t\t\t\t{#if value === option.value}<CheckIcon />{/if}\n\t\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t</Command.Item>\n\t\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\t</Command.Group>\n\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t{#if customValue}\n\t\t\t\t\t\t\t<Command.Separator />\n\t\t\t\t\t\t\t<Command.Group>\n\t\t\t\t\t\t\t\t<Command.Item value={customValue} onSelect={() => choose(customValue, null)}>\n\t\t\t\t\t\t\t\t\t<PlusIcon />\n\t\t\t\t\t\t\t\t\t<span class=\"min-w-0 flex-1 truncate\">{customLabel(customValue)}</span>\n\t\t\t\t\t\t\t\t</Command.Item>\n\t\t\t\t\t\t\t</Command.Group>\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t{/if}\n\t\t\t\t</Command.List>\n\t\t\t</Command.Root>\n\t\t</Popover.Content>\n\t</Popover.Root>\n{:else}\n\t<Input\n\t\t{id}\n\t\t{name}\n\t\tbind:value\n\t\tlist={options.length > 0 ? listId : undefined}\n\t\tmaxlength={maxLength}\n\t\tautocomplete=\"off\"\n\t\t{placeholder}\n\t\t{required}\n\t\t{disabled}\n\t\taria-invalid={invalid}\n\t\tclass={className}\n\t/>\n\t{#if options.length > 0}\n\t\t<datalist id={listId}>\n\t\t\t{#each options as option (option.value)}\n\t\t\t\t<option value={option.value}>{option.label}</option>\n\t\t\t{/each}\n\t\t</datalist>\n\t{/if}\n{/if}\n",
			"type": "registry:file",
			"target": "combobox/combobox.svelte"
		},
		{
			"content": "export interface ComboboxOption {\n\tdisabled?: boolean;\n\tgroup?: string;\n\t/** Additional text used by Command's client-side filter. */\n\tkeywords?: string;\n\tlabel: string;\n\tvalue: string;\n}\n\nexport interface ComboboxOptionGroup<T extends ComboboxOption> {\n\tlabel: string;\n\toptions: T[];\n}\n\nexport function groupComboboxOptions<T extends ComboboxOption>(\n\toptions: readonly T[]\n): ComboboxOptionGroup<T>[] {\n\tconst grouped = new Map<string, T[]>();\n\tfor (const option of options) {\n\t\tconst key = option.group ?? '';\n\t\tgrouped.set(key, [...(grouped.get(key) ?? []), option]);\n\t}\n\treturn [...grouped].map(([label, groupOptions]) => ({ label, options: groupOptions }));\n}\n\nexport function customComboboxValue(\n\toptions: readonly ComboboxOption[],\n\tsearch: string,\n\tallowCustom: boolean\n): string {\n\tif (!allowCustom) return '';\n\tconst typed = search.trim();\n\tif (!typed) return '';\n\tconst normalized = typed.toLocaleLowerCase();\n\tconst exists = options.some(\n\t\t(option) =>\n\t\t\toption.value.toLocaleLowerCase() === normalized ||\n\t\t\toption.label.toLocaleLowerCase() === normalized\n\t);\n\treturn exists ? '' : typed;\n}\n",
			"type": "registry:file",
			"target": "combobox/combobox.ts"
		},
		{
			"content": "export type { ComboboxOption, ComboboxOptionGroup } from \"./combobox.js\";\nexport { default as Combobox } from \"./combobox.svelte\";\n",
			"type": "registry:file",
			"target": "combobox/index.ts"
		}
	]
}