"use client";
import { ChevronDown } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useEffect, useRef, useState } from "react";
const ROTATION_ANGLE_OPEN = 180;
export type DropdownItem = {
id: string | number;
label: string;
icon?: React.ReactNode;
};
export type BasicDropdownProps = {
label: string;
items: DropdownItem[];
onChange?: (item: DropdownItem) => void;
className?: string;
};
export default function BasicDropdown({
label,
items,
onChange,
className = "",
}: BasicDropdownProps) {
const [isOpen, setIsOpen] = useState(false);
const [selectedItem, setSelectedItem] = useState<DropdownItem | null>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
const handleItemSelect = (item: DropdownItem) => {
setSelectedItem(item);
setIsOpen(false);
onChange?.(item);
};
// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
return (
<div className={`relative inline-block ${className}`} ref={dropdownRef}>
<button
className="flex w-full items-center justify-between gap-2 rounded-lg border bg-background px-4 py-2 text-left transition-colors hover:bg-primary"
onClick={() => setIsOpen(!isOpen)}
type="button"
>
<span className="block truncate">
{selectedItem ? selectedItem.label : label}
</span>
<motion.div
animate={{ rotate: isOpen ? ROTATION_ANGLE_OPEN : 0 }}
transition={{ duration: 0.2 }}
>
<ChevronDown className="h-4 w-4" />
</motion.div>
</button>
<AnimatePresence>
{isOpen && (
<motion.div
animate={{ opacity: 1, y: 0, scaleY: 1 }}
className="absolute left-0 z-10 mt-1 w-full origin-top rounded-lg border bg-background shadow-lg"
exit={{
opacity: 0,
y: -10,
scaleY: 0.8,
transition: { duration: 0.2 },
}}
initial={{ opacity: 0, y: -10, scaleY: 0.8 }}
transition={{ type: "spring", bounce: 0.15, duration: 0.4 }}
>
<ul aria-labelledby="dropdown-button" className="py-2">
{items.map((item) => (
<motion.li
animate={{ opacity: 1, x: 0 }}
className="block"
exit={{ opacity: 0, x: -10 }}
initial={{ opacity: 0, x: -10 }}
key={item.id}
role="menuitem"
transition={{ type: "spring", stiffness: 300, damping: 30 }}
whileHover={{ x: 5 }}
>
<button
className={`flex w-full items-center px-4 py-2 text-left text-sm ${
selectedItem?.id === item.id
? "font-medium text-brand"
: ""
}`}
onClick={() => handleItemSelect(item)}
type="button"
>
{item.icon && <span className="mr-2">{item.icon}</span>}
{item.label}
{selectedItem?.id === item.id && (
<motion.span
animate={{ scale: 1 }}
className="ml-auto"
initial={{ scale: 0 }}
transition={{
type: "spring",
stiffness: 300,
damping: 20,
}}
>
<svg
className="h-4 w-4 text-brand"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<title>Selected</title>
<path
d="M5 13l4 4L19 7"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
/>
</svg>
</motion.span>
)}
</button>
</motion.li>
))}
</ul>
</motion.div>
)}
</AnimatePresence>
</div>
);
}