Docs
Dropdown Menu

Dropdown Menu

A fancy dropdown component that can be used to display multiple items.

components/snippet/dropdown-menu.tsx
"use client";
 
import Link from "next/link";
import { useState, useEffect } from "react";
import { useAnimate, stagger, motion } from "framer-motion";
import { ChevronRightIcon, SettingsIcon } from "lucide-react";
import { cn } from "@/lib/utils";
 
function useMenuAnimation(isOpen: boolean) {
    const [scope, animate] = useAnimate();
 
    const staggerMenuItems = stagger(0.1, { startDelay: 0.15 });
 
    useEffect(() => {
        animate("#menu-icon", { rotate: isOpen ? 180 : 0 }, { duration: 0.2 });
 
        animate(
            "ul",
            {
                clipPath: isOpen
                    ? "inset(0% 0% 0% 0% round 12px)"
                    : "inset(10% 50% 90% 50% round 12px)",
            },
            {
                type: "spring",
                bounce: 0,
                duration: 0.5,
            }
        );
 
        animate(
            "li",
            isOpen
                ? { opacity: 1, scale: 1, filter: "blur(0px)" }
                : { opacity: 0, scale: 0.3, filter: "blur(20px)" },
            {
                duration: 0.2,
                delay: isOpen ? staggerMenuItems : 0,
            }
        );
    }, [isOpen, animate, staggerMenuItems]);
 
    return scope;
}
 
type DropdownMenuProps = {
    items: { icon: React.ReactNode; name: string; customStyle?: string }[];
    containerClassName?: string;
    itemClassName?: string;
};
 
export default function DropdownMenu({
    items,
    containerClassName,
    itemClassName,
}: DropdownMenuProps) {
    const [isOpen, setIsOpen] = useState(false);
    const scope = useMenuAnimation(isOpen);
 
 
    return (
        <nav
            className={cn(
                "max-w-[200px] w-full mx-auto space-y-2",
                containerClassName
            )}
            ref={scope}
        >
            <motion.button
                whileTap={{ scale: 0.97 }}
                className="bg-neutral-900 border border-neutral-800 max-w-[300px] w-full flex items-center justify-between p-2.5 rounded-xl"
                onClick={() => setIsOpen((prevState) => !prevState)}
            >
                <span className="text-sm font-medium text-neutral-300">Settings</span>
                <div style={{ transformOrigin: "50% 55%" }}>
                    <SettingsIcon size={14} className="text-neutral-400" id="menu-icon" />
                </div>
            </motion.button>
            <ul
                className={cn(
                    "absolute z-[1] max-w-[200px] w-full space-y-3 p-2.5 bg-neutral-900 border border-neutral-800 rounded-xl",
                    isOpen ? "pointer-events-auto" : "pointer-events-none"
                )}
                style={{
                    clipPath: "inset(10% 50% 90% 50% round 12px)",
                }}
            >
                {items.map(({ icon, name, customStyle }) => (
                    <li key={name}>
                        <Link
                            href="" // Where you will be sent
                            className={cn(
                                "group flex items-center gap-2 rounded-md border border-transparent text-neutral-400 hover:text-neutral-300 focus-visible:text-neutral-300 focus-visible:border-neutral-800 focus-visible:outline-none",
                                itemClassName,
                                customStyle
                            )}
                        >
                            <span>{icon}</span>
                            <span className="flex items-center gap-1 text-sm font-medium">
                                {name}
                                <ChevronRightIcon
                                    size={12}
                                    className="-translate-x-1 scale-0 opacity-0 group-hover:opacity-100 group-hover:scale-100 group-hover:translate-x-0 transition-all"
                                />
                            </span>
                        </Link>
                    </li>
                ))}
            </ul>
        </nav>
    );
}