"use client" import { useState, useRef, useEffect, useCallback } from "react" import { ChevronDown, Check } from "lucide-react" import { cn } from "@/lib/utils" export interface SelectOption { value: string label: string } interface SelectProps { name?: string value?: string defaultValue?: string onChange?: (value: string) => void options: SelectOption[] placeholder?: string required?: boolean className?: string } export function Select({ name, value, defaultValue = "", onChange, options, placeholder = "Select…", required, className, }: SelectProps) { const isControlled = value !== undefined const [internal, setInternal] = useState(defaultValue) const current = isControlled ? value : internal const [open, setOpen] = useState(false) const [cursor, setCursor] = useState(-1) const ref = useRef(null) const listRef = useRef(null) // Close on outside click useEffect(() => { function onOutside(e: MouseEvent) { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false) } document.addEventListener("mousedown", onOutside) return () => document.removeEventListener("mousedown", onOutside) }, []) // Reset cursor when opening useEffect(() => { if (open) { const idx = options.findIndex((o) => o.value === current) setCursor(idx >= 0 ? idx : 0) } }, [open]) // Scroll cursor into view useEffect(() => { if (!open) return const el = listRef.current?.querySelector(`[data-idx="${cursor}"]`) as HTMLElement el?.scrollIntoView({ block: "nearest" }) }, [cursor, open]) function pick(val: string) { if (!isControlled) setInternal(val) onChange?.(val) setOpen(false) } function onKeyDown(e: React.KeyboardEvent) { if (!open) { if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") { e.preventDefault() setOpen(true) } return } switch (e.key) { case "ArrowDown": e.preventDefault() setCursor((v) => Math.min(v + 1, options.length - 1)) break case "ArrowUp": e.preventDefault() setCursor((v) => Math.max(v - 1, 0)) break case "Enter": e.preventDefault() if (cursor >= 0 && options[cursor]) pick(options[cursor].value) break case "Escape": e.preventDefault() setOpen(false) break default: { // Jump to first option starting with typed letter const char = e.key.toLowerCase() if (char.length === 1) { const idx = options.findIndex((o) => o.label.toLowerCase().startsWith(char)) if (idx >= 0) setCursor(idx) } } } } const selected = options.find((o) => o.value === current) return (
{/* Hidden input for FormData / native form submission */} {name && ( )} {/* Trigger button */} {/* Dropdown */} {open && (
{options.map((opt, idx) => { const active = current === opt.value const highlighted = cursor === idx return ( ) })}
)}
) }