"use client" import { useEffect, useMemo, useState, useId, type ReactNode } from "react" import { ChevronsUpDown, Check, X } from "lucide-react" import { Input } from "@/components/ui/input" import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" import { ScrollArea } from "@/components/ui/scroll-area" import { Separator } from "@/components/ui/separator" import { cn } from "@/lib/utils" export type SearchableComboboxOption = { value: string label: string description?: string keywords?: string[] disabled?: boolean } type SearchableComboboxProps = { value: string | null onValueChange: (value: string | null) => void options: SearchableComboboxOption[] placeholder?: string searchPlaceholder?: string emptyText?: string className?: string triggerClassName?: string disabled?: boolean allowClear?: boolean clearLabel?: string renderValue?: (option: SearchableComboboxOption | null) => React.ReactNode renderOption?: (option: SearchableComboboxOption, active: boolean) => React.ReactNode contentClassName?: string scrollClassName?: string scrollProps?: React.HTMLAttributes prefix?: ReactNode } export function SearchableCombobox({ value, onValueChange, options, placeholder = "Selecionar...", searchPlaceholder = "Buscar...", emptyText = "Nenhuma opção encontrada.", className, triggerClassName, disabled, allowClear = false, clearLabel = "Limpar seleção", renderValue, renderOption, contentClassName, scrollClassName, scrollProps, prefix, }: SearchableComboboxProps) { const [open, setOpen] = useState(false) const [search, setSearch] = useState("") const listId = useId() const selected = useMemo(() => { if (value === null) return null return options.find((option) => option.value === value) ?? null }, [options, value]) const filtered = useMemo(() => { const term = search.trim().toLowerCase() if (!term) { return options } return options.filter((option) => { const labelMatch = option.label.toLowerCase().includes(term) const descriptionMatch = option.description?.toLowerCase().includes(term) ?? false const keywordMatch = option.keywords?.some((keyword) => keyword.toLowerCase().includes(term)) ?? false return labelMatch || descriptionMatch || keywordMatch }) }, [options, search]) useEffect(() => { if (!open) { setSearch("") } }, [open]) const handleSelect = (nextValue: string) => { if (nextValue === value) { setOpen(false) return } onValueChange(nextValue) setOpen(false) } return (
setSearch(event.target.value)} placeholder={searchPlaceholder} className="h-9" />
{allowClear ? ( <> ) : null} {filtered.length === 0 ? (
{emptyText}
) : (
{filtered.map((option) => { const isActive = option.value === value const content = renderOption ? ( renderOption(option, isActive) ) : (
{option.label} {option.description ? {option.description} : null}
) return ( ) })}
)}
) }