import { useState, useRef, useEffect, useCallback } from "react"; import { ChevronDown, Check } from "lucide-react"; import { cn } from "@/lib/utils"; export function Select({ value, onValueChange, children, className, id, disabled, }: SelectProps) { const [open, setOpen] = useState(false); const [highlightedIndex, setHighlightedIndex] = useState(-1); const containerRef = useRef(null); const listRef = useRef(null); const options: SelectOptionData[] = []; flattenChildren(children, options); const selectedOption = options.find((o) => o.value === value); const displayLabel = selectedOption?.label ?? value ?? ""; const close = useCallback(() => { setOpen(false); setHighlightedIndex(-1); }, []); useEffect(() => { if (!open) return; const handler = (e: MouseEvent) => { if (containerRef.current && !containerRef.current.contains(e.target as Node)) { close(); } }; document.addEventListener("mousedown", handler); return () => document.removeEventListener("mousedown", handler); }, [open, close]); useEffect(() => { if (open && listRef.current && highlightedIndex >= 0) { const el = listRef.current.children[highlightedIndex] as HTMLElement | undefined; el?.scrollIntoView({ block: "nearest" }); } }, [open, highlightedIndex]); const handleKeyDown = (e: React.KeyboardEvent) => { if (disabled) return; switch (e.key) { case "Enter": case " ": e.preventDefault(); if (!open) { setOpen(true); setHighlightedIndex(options.findIndex((o) => o.value === value)); } else if (highlightedIndex >= 0 && options[highlightedIndex]) { onValueChange?.(options[highlightedIndex].value); close(); } break; case "ArrowDown": e.preventDefault(); if (!open) { setOpen(true); setHighlightedIndex(options.findIndex((o) => o.value === value)); } else { setHighlightedIndex((i) => Math.min(i + 1, options.length - 1)); } break; case "ArrowUp": e.preventDefault(); if (open) { setHighlightedIndex((i) => Math.max(i - 1, 0)); } break; case "Escape": e.preventDefault(); close(); break; } }; return (
{open && (
{options.map((opt, i) => { const isSelected = opt.value === value; const isHighlighted = i === highlightedIndex; return (
setHighlightedIndex(i)} onClick={() => { onValueChange?.(opt.value); close(); }} className={cn( "flex items-center gap-2 px-3 py-2 text-sm font-courier cursor-pointer transition-colors", isHighlighted && "bg-foreground/10", isSelected && "text-foreground", !isSelected && "text-muted-foreground", )} > {opt.label}
); })}
)}
); } export function SelectOption(_props: SelectOptionProps) { return null; } function flattenChildren(children: React.ReactNode, out: SelectOptionData[]) { const arr = Array.isArray(children) ? children : [children]; for (const child of arr) { if (!child || typeof child !== "object" || !("props" in child)) continue; const props = child.props as Record; if (props.value !== undefined) { out.push({ value: String(props.value), label: typeof props.children === "string" ? props.children : String(props.value), }); } else if (props.children) { flattenChildren(props.children as React.ReactNode, out); } } } interface SelectProps { value?: string; onValueChange?: (value: string) => void; children?: React.ReactNode; className?: string; id?: string; disabled?: boolean; } interface SelectOptionProps { value: string; children: React.ReactNode; } interface SelectOptionData { value: string; label: string; }