1
9import * as React from "react";
10import * as RadixMenu from "@radix-ui/react-dropdown-menu";
11import { clsx } from "clsx";
12
13export const DropdownMenu = RadixMenu.Root;
14export const DropdownMenuTrigger = RadixMenu.Trigger;
15export const DropdownMenuGroup = RadixMenu.Group;
16export const DropdownMenuPortal = RadixMenu.Portal;
17export const DropdownMenuRadioGroup = RadixMenu.RadioGroup;
18
19export const DropdownMenuContent = React.forwardRef<
20 React.ElementRef<typeof RadixMenu.Content>,
21 React.ComponentPropsWithoutRef<typeof RadixMenu.Content>
22>(({ className, sideOffset = 4, ...props }, ref) => (
23 <RadixMenu.Portal>
24 <RadixMenu.Content
25 ref={ref}
26 sideOffset={sideOffset}
27 className={clsx(
28 "z-50 min-w-[10rem] rounded-md border border-border bg-popover p-1",
29 "text-popover-foreground shadow-md focus:outline-none",
30 className,
31 )}
32 {...props}
33 />
34 </RadixMenu.Portal>
35));
36DropdownMenuContent.displayName = "DropdownMenuContent";
37
38export const DropdownMenuItem = React.forwardRef<
39 React.ElementRef<typeof RadixMenu.Item>,
40 React.ComponentPropsWithoutRef<typeof RadixMenu.Item>
41>(({ className, ...props }, ref) => (
42 <RadixMenu.Item
43 ref={ref}
44 className={clsx(
45 "relative flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm",
46 "outline-none data-[highlighted]:bg-accent data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
47 className,
48 )}
49 {...props}
50 />
51));
52DropdownMenuItem.displayName = "DropdownMenuItem";
53
54export const DropdownMenuCheckboxItem = React.forwardRef<
55 React.ElementRef<typeof RadixMenu.CheckboxItem>,
56 React.ComponentPropsWithoutRef<typeof RadixMenu.CheckboxItem>
57>(({ className, children, checked, ...props }, ref) => (
58 <RadixMenu.CheckboxItem
59 ref={ref}
60 checked={checked ?? false}
61 className={clsx(
62 "flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm",
63 "outline-none data-[highlighted]:bg-accent",
64 className,
65 )}
66 {...props}
67 >
68 <RadixMenu.ItemIndicator>✓</RadixMenu.ItemIndicator>
69 {children}
70 </RadixMenu.CheckboxItem>
71));
72DropdownMenuCheckboxItem.displayName = "DropdownMenuCheckboxItem";
73
74export const DropdownMenuLabel = ({
75 className,
76 ...props
77}: React.HTMLAttributes<HTMLDivElement>) => (
78 <div className={clsx("px-2 py-1.5 text-xs text-muted-foreground", className)} {...props} />
79);
80
81export const DropdownMenuSeparator = ({
82 className,
83 ...props
84}: React.ComponentPropsWithoutRef<typeof RadixMenu.Separator>) => (
85 <RadixMenu.Separator className={clsx("my-1 h-px bg-border", className)} {...props} />
86);
87
88export interface MenuState {
89 open: boolean;
90 anchor: { x: number; y: number } | null;
91 openAt(x: number, y: number): void;
92 close(): void;
93}
94
95
96export function useMenuState(): MenuState {
97 const [open, setOpen] = React.useState(false);
98 const [anchor, setAnchor] = React.useState<{ x: number; y: number } | null>(null);
99 const openAt = React.useCallback((x: number, y: number) => {
100 setAnchor({ x, y });
101 setOpen(true);
102 }, []);
103 const close = React.useCallback(() => setOpen(false), []);
104 return { open, anchor, openAt, close } as const;
105}
106