"use client"; import * as React from "react"; import { Check, ChevronsUpDown } from "lucide-react"; import { cn } from "@/src/utils/tailwind"; import { Button } from "@/src/components/ui/button"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "@/src/components/ui/command"; import { Popover, PopoverContent, PopoverTrigger, } from "@/src/components/ui/popover"; export interface ComboboxOption< T extends string | number | boolean | { id: string }, > { value: T; label?: string; disabled?: boolean; } export interface ComboboxOptionGroup< T extends string | number | boolean | { id: string }, > { heading?: string; options: ComboboxOption[]; } export type ComboboxOptionsInput< T extends string | number | boolean | { id: string }, > = ComboboxOption[] | ComboboxOptionGroup[]; export interface ComboboxProps< T extends string | number | boolean | { id: string }, > { options: ComboboxOptionsInput; value?: T; onValueChange?: (value: T) => void; placeholder?: string; emptyText?: string; searchPlaceholder?: string; disabled?: boolean; className?: string; name?: string; } function isGroupedOptions( options: ComboboxOptionsInput, ): options is ComboboxOptionGroup[] { return ( options.length > 0 && typeof options[0] === "object" && "options" in options[0] && Array.isArray(options[0].options) ); } function isEqual( a: T | undefined, b: T | undefined, ): boolean { if ( a && b && typeof a === "object" && typeof b === "object" && "id" in a && "id" in b ) { return (a as { id: string }).id === (b as { id: string }).id; } return a === b; } export function Combobox({ options, value, onValueChange, placeholder = "Select option...", emptyText = "No option found.", searchPlaceholder = "Search...", disabled = false, className, name, }: ComboboxProps) { const [open, setOpen] = React.useState(false); const selectedOption = React.useMemo(() => { if (isGroupedOptions(options)) { for (const group of options) { const found = group.options.find((opt) => isEqual(opt.value, value)); if (found) return found; } return undefined; } return options.find((option) => isEqual(option.value, value)); }, [options, value]); return ( {emptyText} {isGroupedOptions(options) ? ( // Render with groups options.map((group, groupIndex) => ( {group.options.map((option) => ( { if (!option.disabled && onValueChange) { onValueChange(option.value as T); setOpen(false); } }} className={cn( "text-xs", option.disabled && "text-muted-foreground line-through", )} > {option.label ?? String(option.value)} ))} )) ) : ( // Flat rendering (backward compatible) {options.map((option) => ( { if (!option.disabled && onValueChange) { onValueChange(option.value as T); setOpen(false); } }} className={cn( "text-xs", option.disabled && "text-muted-foreground line-through", )} > {option.label ?? String(option.value)} ))} )} ); }