Dropdown Menu
Dropdown Menu é um componente de navegação para React e Tailwind CSS, da biblioteca SmoothUI, com licença MIT. Copia e cola no teu projeto.
SmoothUI
MIT
navigation
O que é
Dropdown Menu é um componente de navegação para React e Tailwind CSS, da biblioteca SmoothUI, com licença MIT. Copia e cola no teu projeto.
Como usar
Abre o componente no catálogo, carrega em copiar, e cola no teu projeto. O prompt copiado leva o código e as regras para o teu agente o reproduzir sem alterar nada.
Cinco cópias grátis por mês. Sem cartão.
Precisa de
npm i motion
Licença
Este componente vem de SmoothUI e é redistribuído sob a licença MIT, que permite uso comercial. O aviso de copyright viaja com o código.
Dropdown Menu
"use client";
import {
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenu as DropdownMenuRoot,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
import { motion, useReducedMotion } from "motion/react";
import type React from "react";
import { useState } from "react";
import { SPRING_DEFAULT } from "@/components/smoothui/lib/animation";
export interface DropdownMenuProps {
/** Alignment of the dropdown relative to the trigger */
align?: "start" | "center" | "end";
/** The trigger element that opens the menu */
children: React.ReactNode;
/** Optional CSS class for the content container */
className?: string;
/** Menu items to render */
items: DropdownMenuItemConfig[];
/** Callback when open state changes */
onOpenChange?: (open: boolean) => void;
/** Controlled open state */
open?: boolean;
/** Side offset for the dropdown content */
sideOffset?: number;
}
export interface DropdownMenuItemConfig {
/** Nested submenu items */
children?: DropdownMenuItemConfig[];
/** Whether the item is disabled */
disabled?: boolean;
/** Renders a group label instead of an item */
groupLabel?: string;
/** Optional icon to display before the label */
icon?: React.ReactNode;
/** Unique key for the item */
key: string;
/** Display label */
label: string;
/** Callback when item is selected */
onSelect?: () => void;
/** Renders a separator instead of an item */
separator?: boolean;
/** Optional keyboard shortcut text to display */
shortcut?: string;
/** Destructive variant styling */
variant?: "default" | "destructive";
}
export default function DropdownMenu({
children,
items,
className,
open,
onOpenChange,
sideOffset = 4,
align = "start",
}: DropdownMenuProps) {
const shouldReduceMotion = useReducedMotion();
const [isOpen, setIsOpen] = useState(false);
const controlledOpen = open ?? isOpen;
const handleOpenChange = (value: boolean) => {
setIsOpen(value);
onOpenChange?.(value);
};
const renderItem = (item: DropdownMenuItemConfig, index: number) => {
if (item.separator) {
return <DropdownMenuSeparator key={item.key} />;
}
if (item.groupLabel) {
return (
<DropdownMenuLabel key={item.key}>{item.groupLabel}</DropdownMenuLabel>
);
}
if (item.children && item.children.length > 0) {
return (
<DropdownMenuSub key={item.key}>
<DropdownMenuSubTrigger disabled={item.disabled}>
{item.icon ? <span className="mr-2">{item.icon}</span> : null}
{item.label}
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
{item.children.map((child, childIndex) =>
renderItem(child, childIndex)
)}
</DropdownMenuSubContent>
</DropdownMenuSub>
);
}
return (
<motion.div
animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }}
initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: -4 }}
key={item.key}
transition={
shouldReduceMotion
? { duration: 0 }
: { ...SPRING_DEFAULT, delay: index * 0.02 }
}
>
<DropdownMenuItem
disabled={item.disabled}
onSelect={item.onSelect}
variant={item.variant}
>
{item.icon ? <span className="mr-2">{item.icon}</span> : null}
{item.label}
{item.shortcut ? (
<DropdownMenuShortcut>{item.shortcut}</DropdownMenuShortcut>
) : null}
</DropdownMenuItem>
</motion.div>
);
};
return (
<DropdownMenuRoot onOpenChange={handleOpenChange} open={controlledOpen}>
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
<DropdownMenuContent
align={align}
className={cn("origin-top", className)}
sideOffset={sideOffset}
>
<motion.div
animate={
shouldReduceMotion ? { opacity: 1 } : { opacity: 1, scale: 1, y: 0 }
}
initial={
shouldReduceMotion
? { opacity: 1 }
: { opacity: 0, scale: 0.95, y: -4 }
}
transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}
>
<DropdownMenuGroup>
{items.map((item, index) => renderItem(item, index))}
</DropdownMenuGroup>
</motion.div>
</DropdownMenuContent>
</DropdownMenuRoot>
);
}