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