"use client" import * as React from "react" import { Select as SelectPrimitive } from "@base-ui/react/select" import { CheckIcon, ChevronDownIcon, ChevronUpIcon, LoaderCircleIcon, SearchIcon, XIcon } from "lucide-react" import { cn, stopInteractivePropagation } from "@/lib/utils" export type SelectRootProps = React.ComponentPropsWithoutRef export type SelectGroupProps = SelectPrimitive.Group.Props export type SelectValueProps = SelectPrimitive.Value.Props export type SelectTriggerProps = SelectPrimitive.Trigger.Props & { size?: "sm" | "default" | "lg" } export type SelectContentProps = SelectPrimitive.Popup.Props & Pick< SelectPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger" > export type SelectLabelProps = SelectPrimitive.GroupLabel.Props export type SelectItemProps = SelectPrimitive.Item.Props & { showIndicator?: boolean } export type SelectSeparatorProps = SelectPrimitive.Separator.Props export type SelectScrollUpButtonProps = React.ComponentProps export type SelectScrollDownButtonProps = React.ComponentProps export type SelectOption = { label: React.ReactNode value: string disabled?: boolean description?: React.ReactNode keywords?: string[] } export type SelectOptionGroup = { label?: React.ReactNode options: SelectOption[] } export type SelectProps = Omit & { value?: string | null defaultValue?: string | null onValueChange?: (value: string | undefined) => void options?: SelectOption[] groups?: SelectOptionGroup[] placeholder?: string searchPlaceholder?: string emptyLabel?: React.ReactNode emptyMessage?: React.ReactNode clearLabel?: string size?: "sm" | "default" | "lg" clearable?: boolean searchable?: boolean loading?: boolean loadingLabel?: React.ReactNode disabled?: boolean showSelectedDescription?: boolean triggerClassName?: string contentClassName?: string itemClassName?: string searchClassName?: string renderOption?: (option: SelectOption, state: { selected: boolean }) => React.ReactNode showSelectedIndicator?: boolean invalid?: boolean onSearchChange?: (value: string) => void } const SelectRoot = SelectPrimitive.Root function optionMatchesSearch(option: SelectOption, search: string) { const normalized = search.trim().toLowerCase() if (!normalized) return true const labelText = typeof option.label === "string" || typeof option.label === "number" ? String(option.label) : option.value const haystack = [labelText, option.value, ...(option.keywords ?? [])].join(" ").toLowerCase() return haystack.includes(normalized) } function Select({ value, defaultValue, onValueChange, options, groups, placeholder = "Select", searchPlaceholder = "Search options...", emptyLabel = "No options found", emptyMessage, clearLabel = "Clear selection", size = "default", clearable = false, searchable = false, loading = false, loadingLabel = "Loading options...", disabled = false, showSelectedDescription = true, triggerClassName, contentClassName, itemClassName, searchClassName, renderOption, showSelectedIndicator = true, invalid, children, onOpenChange, onSearchChange, ...props }: SelectProps) { const hasCustomOptions = Boolean(options || groups) const [search, setSearch] = React.useState("") const optionGroups = React.useMemo( () => (hasCustomOptions ? groups ?? [{ options: options ?? [] }] : []), [groups, hasCustomOptions, options] ) const flatOptions = React.useMemo( () => optionGroups.flatMap((group) => group.options), [optionGroups] ) const selectedOption = flatOptions.find((option) => option.value === value) const filteredGroups = optionGroups .map((group) => ({ ...group, options: group.options.filter((option) => optionMatchesSearch(option, search)), })) .filter((group) => group.options.length > 0) const filteredOptionsCount = filteredGroups.reduce((count, group) => count + group.options.length, 0) if (!hasCustomOptions) { return ( onValueChange?.((nextValue as string | null | undefined) ?? undefined)} onOpenChange={onOpenChange} disabled={disabled} {...props} > {children} ) } return ( onValueChange?.((nextValue as string | null | undefined) ?? undefined)} disabled={disabled || loading} onOpenChange={(open, eventDetails) => { if (!open) setSearch("") onOpenChange?.(open, eventDetails) }} {...props} > {selectedOption ? ( {selectedOption.label} {showSelectedDescription && selectedOption.description ? ( {selectedOption.description} ) : null} ) : null} {loading ? : null} {clearable && value && !disabled && !loading ? ( { event.preventDefault() stopInteractivePropagation(event) onValueChange?.(undefined) }} onMouseDown={stopInteractivePropagation} onDoubleClick={stopInteractivePropagation} onKeyDown={(event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault() onValueChange?.(undefined) } }} > ) : null} {searchable ? (
{ setSearch(event.target.value) onSearchChange?.(event.target.value) }} placeholder={searchPlaceholder} className={cn( "min-w-0 flex-1 bg-transparent outline-none placeholder:text-muted-foreground", searchClassName )} />
) : null} {loading ? (
{loadingLabel}
) : filteredOptionsCount === 0 ? (
{emptyMessage ?? emptyLabel}
) : ( filteredGroups.map((group, groupIndex) => ( {group.label ? {group.label} : null} {group.options.map((option) => { const selected = option.value === value return ( {renderOption ? ( renderOption(option, { selected }) ) : ( {option.label} {option.description ? ( {option.description} ) : null} )} ) })} )) )}
) } function SelectGroup({ className, ...props }: SelectGroupProps) { return ( ) } function SelectValue({ className, ...props }: SelectValueProps) { return ( ) } function SelectTrigger({ className, size = "default", children, ...props }: SelectTriggerProps) { return ( {children} } /> ) } function SelectContent({ className, children, side = "bottom", sideOffset = 4, align = "center", alignOffset = 0, alignItemWithTrigger = true, ...props }: SelectContentProps) { return ( {children} ) } function SelectLabel({ className, ...props }: SelectLabelProps) { return ( ) } function SelectItem({ className, children, showIndicator = true, ...props }: SelectItemProps) { return ( {children} {showIndicator ? ( } > ) : null} ) } function SelectSeparator({ className, ...props }: SelectSeparatorProps) { return ( ) } function SelectScrollUpButton({ className, ...props }: SelectScrollUpButtonProps) { return ( ) } function SelectScrollDownButton({ className, ...props }: SelectScrollDownButtonProps) { return ( ) } export { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, }