Full Radix menu kit — items, checkbox and radio items, submenus, kbd shortcuts — with origin-aware physics and 4px submenu slides.
npx shadcn@latest add @paragon/dropdown-menu"use client";
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { Check, ChevronRight, Circle } from "lucide-react";
import { cn } from "@/lib/utils";
/*
* Menu physics: origin-aware 0.97 -> 1 scale over 200ms ease-out,
* 150ms non-retracing exit. Submenus slide 4px away from their trigger
* side (data-side comes from Radix) with a 150ms enter / 100ms exit.
* Reduced motion keeps only the fades.
*/
const dropdownMenuStyles = `
@keyframes pg-menu-in { from { opacity: 0; scale: 0.97; } }
@keyframes pg-menu-out { to { opacity: 0; scale: 0.99; } }
@keyframes pg-menu-sub-in-right { from { opacity: 0; translate: -4px 0; } }
@keyframes pg-menu-sub-in-left { from { opacity: 0; translate: 4px 0; } }
@keyframes pg-menu-sub-out { to { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
@keyframes pg-menu-in { from { opacity: 0; } }
@keyframes pg-menu-out { to { opacity: 0; } }
@keyframes pg-menu-sub-in-right { from { opacity: 0; } }
@keyframes pg-menu-sub-in-left { from { opacity: 0; } }
}
`;
function DropdownMenu(
props: React.ComponentProps<typeof DropdownMenuPrimitive.Root>,
) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
}
function DropdownMenuTrigger(
props: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>,
) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
);
}
function DropdownMenuPortal(
props: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>,
) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
);
}
function DropdownMenuGroup(
props: React.ComponentProps<typeof DropdownMenuPrimitive.Group>,
) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
);
}
function DropdownMenuContent({
className,
sideOffset = 6,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<>
{/* Hoisted outside the Portal: React 19 keeps a hoistable <style> as a
child node, and the Radix menu Portal enforces a single child. */}
<style href="paragon-dropdown-menu" precedence="paragon">{dropdownMenuStyles}</style>
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"z-50 min-w-40 origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-lg bg-popover p-1 text-popover-foreground shadow-overlay",
"data-[state=open]:animate-[pg-menu-in_200ms_var(--ease-out)]",
"data-[state=closed]:animate-[pg-menu-out_150ms_var(--ease-exit)_forwards]",
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
</>
);
}
const menuItemClasses = cn(
"relative flex cursor-default items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none select-none",
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground",
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
"[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground",
);
export interface DropdownMenuItemProps
extends React.ComponentProps<typeof DropdownMenuPrimitive.Item> {
/** Indent to align with items that carry a leading indicator. */
inset?: boolean;
variant?: "default" | "destructive";
}
function DropdownMenuItem({
className,
inset = false,
variant = "default",
...props
}: DropdownMenuItemProps) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-variant={variant}
className={cn(
menuItemClasses,
inset && "pl-8",
variant === "destructive" &&
"text-destructive data-[highlighted]:bg-destructive/10 data-[highlighted]:text-destructive [&_svg]:text-destructive",
className,
)}
{...props}
/>
);
}
function DropdownMenuCheckboxItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(menuItemClasses, "pl-8", className)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-4 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
);
}
function DropdownMenuRadioGroup(
props: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>,
) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
);
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(menuItemClasses, "pl-8", className)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-4 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="size-1.5 fill-current text-foreground" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
);
}
function DropdownMenuLabel({
className,
inset = false,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
className={cn(
"px-2 py-1.5 text-xs font-medium text-muted-foreground",
inset && "pl-8",
className,
)}
{...props}
/>
);
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
);
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"kbd">) {
return (
<kbd
data-slot="dropdown-menu-shortcut"
className={cn(
"ml-auto font-sans text-xs tracking-widest text-muted-foreground",
className,
)}
{...props}
/>
);
}
function DropdownMenuSub(
props: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>,
) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
}
function DropdownMenuSubTrigger({
className,
inset = false,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
className={cn(
menuItemClasses,
"data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
inset && "pl-8",
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>
);
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"z-50 min-w-36 origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-lg bg-popover p-1 text-popover-foreground shadow-overlay",
"data-[state=open]:data-[side=right]:animate-[pg-menu-sub-in-right_150ms_var(--ease-out)]",
"data-[state=open]:data-[side=left]:animate-[pg-menu-sub-in-left_150ms_var(--ease-out)]",
"data-[state=closed]:animate-[pg-menu-sub-out_100ms_var(--ease-exit)_forwards]",
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
);
}
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuPortal,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
};