Skip to content

Commit 8c13bdf

Browse files
committed
feat: add ContextMenu component with various subcomponents for enhanced UI interaction
1 parent a6aad97 commit 8c13bdf

File tree

1 file changed

+252
-0
lines changed

1 file changed

+252
-0
lines changed

components/ui/context-menu.tsx

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
"use client"
2+
3+
import * as React from "react"
4+
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
5+
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
6+
7+
import { cn } from "@/lib/utils"
8+
9+
function ContextMenu({
10+
...props
11+
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
12+
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
13+
}
14+
15+
function ContextMenuTrigger({
16+
...props
17+
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
18+
return (
19+
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
20+
)
21+
}
22+
23+
function ContextMenuGroup({
24+
...props
25+
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
26+
return (
27+
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
28+
)
29+
}
30+
31+
function ContextMenuPortal({
32+
...props
33+
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
34+
return (
35+
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
36+
)
37+
}
38+
39+
function ContextMenuSub({
40+
...props
41+
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
42+
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />
43+
}
44+
45+
function ContextMenuRadioGroup({
46+
...props
47+
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
48+
return (
49+
<ContextMenuPrimitive.RadioGroup
50+
data-slot="context-menu-radio-group"
51+
{...props}
52+
/>
53+
)
54+
}
55+
56+
function ContextMenuSubTrigger({
57+
className,
58+
inset,
59+
children,
60+
...props
61+
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
62+
inset?: boolean
63+
}) {
64+
return (
65+
<ContextMenuPrimitive.SubTrigger
66+
data-slot="context-menu-sub-trigger"
67+
data-inset={inset}
68+
className={cn(
69+
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
70+
className
71+
)}
72+
{...props}
73+
>
74+
{children}
75+
<ChevronRightIcon className="ml-auto" />
76+
</ContextMenuPrimitive.SubTrigger>
77+
)
78+
}
79+
80+
function ContextMenuSubContent({
81+
className,
82+
...props
83+
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
84+
return (
85+
<ContextMenuPrimitive.SubContent
86+
data-slot="context-menu-sub-content"
87+
className={cn(
88+
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
89+
className
90+
)}
91+
{...props}
92+
/>
93+
)
94+
}
95+
96+
function ContextMenuContent({
97+
className,
98+
...props
99+
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
100+
return (
101+
<ContextMenuPrimitive.Portal>
102+
<ContextMenuPrimitive.Content
103+
data-slot="context-menu-content"
104+
className={cn(
105+
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
106+
className
107+
)}
108+
{...props}
109+
/>
110+
</ContextMenuPrimitive.Portal>
111+
)
112+
}
113+
114+
function ContextMenuItem({
115+
className,
116+
inset,
117+
variant = "default",
118+
...props
119+
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
120+
inset?: boolean
121+
variant?: "default" | "destructive"
122+
}) {
123+
return (
124+
<ContextMenuPrimitive.Item
125+
data-slot="context-menu-item"
126+
data-inset={inset}
127+
data-variant={variant}
128+
className={cn(
129+
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
130+
className
131+
)}
132+
{...props}
133+
/>
134+
)
135+
}
136+
137+
function ContextMenuCheckboxItem({
138+
className,
139+
children,
140+
checked,
141+
...props
142+
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
143+
return (
144+
<ContextMenuPrimitive.CheckboxItem
145+
data-slot="context-menu-checkbox-item"
146+
className={cn(
147+
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
148+
className
149+
)}
150+
checked={checked}
151+
{...props}
152+
>
153+
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
154+
<ContextMenuPrimitive.ItemIndicator>
155+
<CheckIcon className="size-4" />
156+
</ContextMenuPrimitive.ItemIndicator>
157+
</span>
158+
{children}
159+
</ContextMenuPrimitive.CheckboxItem>
160+
)
161+
}
162+
163+
function ContextMenuRadioItem({
164+
className,
165+
children,
166+
...props
167+
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
168+
return (
169+
<ContextMenuPrimitive.RadioItem
170+
data-slot="context-menu-radio-item"
171+
className={cn(
172+
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
173+
className
174+
)}
175+
{...props}
176+
>
177+
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
178+
<ContextMenuPrimitive.ItemIndicator>
179+
<CircleIcon className="size-2 fill-current" />
180+
</ContextMenuPrimitive.ItemIndicator>
181+
</span>
182+
{children}
183+
</ContextMenuPrimitive.RadioItem>
184+
)
185+
}
186+
187+
function ContextMenuLabel({
188+
className,
189+
inset,
190+
...props
191+
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
192+
inset?: boolean
193+
}) {
194+
return (
195+
<ContextMenuPrimitive.Label
196+
data-slot="context-menu-label"
197+
data-inset={inset}
198+
className={cn(
199+
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
200+
className
201+
)}
202+
{...props}
203+
/>
204+
)
205+
}
206+
207+
function ContextMenuSeparator({
208+
className,
209+
...props
210+
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
211+
return (
212+
<ContextMenuPrimitive.Separator
213+
data-slot="context-menu-separator"
214+
className={cn("bg-border -mx-1 my-1 h-px", className)}
215+
{...props}
216+
/>
217+
)
218+
}
219+
220+
function ContextMenuShortcut({
221+
className,
222+
...props
223+
}: React.ComponentProps<"span">) {
224+
return (
225+
<span
226+
data-slot="context-menu-shortcut"
227+
className={cn(
228+
"text-muted-foreground ml-auto text-xs tracking-widest",
229+
className
230+
)}
231+
{...props}
232+
/>
233+
)
234+
}
235+
236+
export {
237+
ContextMenu,
238+
ContextMenuTrigger,
239+
ContextMenuContent,
240+
ContextMenuItem,
241+
ContextMenuCheckboxItem,
242+
ContextMenuRadioItem,
243+
ContextMenuLabel,
244+
ContextMenuSeparator,
245+
ContextMenuShortcut,
246+
ContextMenuGroup,
247+
ContextMenuPortal,
248+
ContextMenuSub,
249+
ContextMenuSubContent,
250+
ContextMenuSubTrigger,
251+
ContextMenuRadioGroup,
252+
}

0 commit comments

Comments
 (0)