"use client"; import { Check, ChevronDown, ListFilter } from "lucide-react"; import { useEffect, useId, useLayoutEffect, useRef, useState, type ReactNode, } from "react"; import { createPortal } from "react-dom"; export type ProjectSelectOption = { value: T; label: string; icon?: ReactNode; disabled?: boolean; }; type ProjectSelectMenuProps = { value: T; options: readonly ProjectSelectOption[]; onChange: (value: T) => void; ariaLabel: string; variant?: "field" | "compact" | "responsive"; triggerIcon?: ReactNode; disabled?: boolean; className?: string; menuWidth?: number; }; type MenuPosition = { left: number; top: number; width: number; }; export function ProjectSelectMenu({ value, options, onChange, ariaLabel, variant = "field", triggerIcon, disabled = false, className = "", menuWidth, }: ProjectSelectMenuProps) { const [open, setOpen] = useState(false); const [menuPosition, setMenuPosition] = useState(null); const containerRef = useRef(null); const triggerRef = useRef(null); const menuRef = useRef(null); const menuId = useId(); const activeOption = options.find((option) => option.value === value) ?? options[0]; useEffect(() => { if (!open) return; const handlePointerDown = (event: PointerEvent) => { const target = event.target as Node; if (!containerRef.current?.contains(target) && !menuRef.current?.contains(target)) { setOpen(false); setMenuPosition(null); } }; const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") { setOpen(false); setMenuPosition(null); triggerRef.current?.focus(); } }; document.addEventListener("pointerdown", handlePointerDown); document.addEventListener("keydown", handleKeyDown); return () => { document.removeEventListener("pointerdown", handlePointerDown); document.removeEventListener("keydown", handleKeyDown); }; }, [open]); useLayoutEffect(() => { if (!open) return; const updatePosition = () => { const trigger = triggerRef.current; const menu = menuRef.current; if (!trigger || !menu) return; const margin = 12; const gap = 8; const triggerRect = trigger.getBoundingClientRect(); const menuRect = menu.getBoundingClientRect(); const preferredWidth = menuWidth ?? (variant === "compact" ? 240 : Math.max(224, triggerRect.width)); const width = Math.min(preferredWidth, window.innerWidth - margin * 2); const left = Math.min( Math.max(triggerRect.right - width, margin), window.innerWidth - width - margin, ); const fitsBelow = triggerRect.bottom + gap + menuRect.height <= window.innerHeight - margin; const top = fitsBelow ? triggerRect.bottom + gap : Math.max(margin, triggerRect.top - gap - menuRect.height); setMenuPosition({ left, top, width }); }; updatePosition(); window.addEventListener("resize", updatePosition); window.addEventListener("scroll", updatePosition, true); return () => { window.removeEventListener("resize", updatePosition); window.removeEventListener("scroll", updatePosition, true); }; }, [menuWidth, open, variant]); if (!activeOption) return null; const activeIcon = activeOption.icon ?? triggerIcon ??