@arolariu/components 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/EXAMPLES.md +1035 -1035
  2. package/LICENSE.md +21 -21
  3. package/changelog.md +9 -0
  4. package/dist/components/ui/bubble-background.js.map +1 -1
  5. package/dist/components/ui/chart.d.ts +25 -11
  6. package/dist/components/ui/chart.d.ts.map +1 -1
  7. package/dist/components/ui/chart.js +14 -11
  8. package/dist/components/ui/chart.js.map +1 -1
  9. package/dist/components/ui/dropdrawer.js +1 -1
  10. package/dist/components/ui/dropdrawer.js.map +1 -1
  11. package/dist/components/ui/sidebar.js +1 -1
  12. package/dist/components/ui/sidebar.js.map +1 -1
  13. package/dist/components/ui/typewriter.d.ts +18 -0
  14. package/dist/components/ui/typewriter.d.ts.map +1 -0
  15. package/dist/components/ui/typewriter.js +128 -0
  16. package/dist/components/ui/typewriter.js.map +1 -0
  17. package/dist/hooks/{use-mobile.d.ts → useIsMobile.d.ts} +1 -1
  18. package/dist/hooks/useIsMobile.d.ts.map +1 -0
  19. package/dist/hooks/useIsMobile.js +19 -0
  20. package/dist/hooks/useIsMobile.js.map +1 -0
  21. package/dist/hooks/useWindowSize.d.ts +30 -0
  22. package/dist/hooks/useWindowSize.d.ts.map +1 -0
  23. package/dist/hooks/useWindowSize.js +28 -0
  24. package/dist/hooks/useWindowSize.js.map +1 -0
  25. package/dist/index.css +67 -25
  26. package/dist/index.css.map +1 -1
  27. package/dist/index.d.ts +4 -2
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +4 -2
  30. package/dist/lib/utils.js.map +1 -1
  31. package/package.json +52 -42
  32. package/src/components/ui/bubble-background.tsx +189 -189
  33. package/src/components/ui/chart.tsx +67 -35
  34. package/src/components/ui/dropdrawer.tsx +973 -973
  35. package/src/components/ui/sidebar.tsx +1 -1
  36. package/src/components/ui/typewriter.tsx +188 -0
  37. package/src/hooks/{use-mobile.tsx → useIsMobile.tsx} +45 -44
  38. package/src/hooks/useWindowSize.tsx +72 -0
  39. package/src/index.ts +408 -400
  40. package/src/lib/utils.ts +10 -10
  41. package/dist/hooks/use-mobile.d.ts.map +0 -1
  42. package/dist/hooks/use-mobile.js +0 -18
  43. package/dist/hooks/use-mobile.js.map +0 -1
@@ -1,973 +1,973 @@
1
- "use client";
2
-
3
- import { AnimatePresence, motion, Transition } from "motion/react";
4
- import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
5
- import * as React from "react";
6
-
7
- import {
8
- Drawer,
9
- DrawerClose,
10
- DrawerContent,
11
- DrawerFooter,
12
- DrawerHeader,
13
- DrawerTitle,
14
- DrawerTrigger,
15
- } from "@/components/ui/drawer";
16
- import {
17
- DropdownMenu,
18
- DropdownMenuContent,
19
- DropdownMenuItem,
20
- DropdownMenuLabel,
21
- DropdownMenuSeparator,
22
- DropdownMenuSub,
23
- DropdownMenuSubContent,
24
- DropdownMenuSubTrigger,
25
- DropdownMenuTrigger,
26
- } from "@/components/ui/dropdown-menu";
27
- import { useIsMobile } from "@/hooks/use-mobile";
28
- import { cn } from "@/lib/utils";
29
-
30
- const DropDrawerContext = React.createContext<{ isMobile: boolean }>({
31
- isMobile: false,
32
- });
33
-
34
- const useDropDrawerContext = () => {
35
- const context = React.useContext(DropDrawerContext);
36
- if (!context) {
37
- throw new Error(
38
- "DropDrawer components cannot be rendered outside the Context",
39
- );
40
- }
41
- return context;
42
- };
43
-
44
- function DropDrawer({
45
- children,
46
- ...props
47
- }:
48
- | React.ComponentProps<typeof Drawer>
49
- | React.ComponentProps<typeof DropdownMenu>) {
50
- const isMobile = useIsMobile();
51
- const DropdownComponent = isMobile ? Drawer : DropdownMenu;
52
-
53
- return (
54
- <DropDrawerContext.Provider value={{ isMobile }}>
55
- <DropdownComponent
56
- data-slot="drop-drawer"
57
- {...(isMobile && { autoFocus: true })}
58
- {...props}
59
- >
60
- {children}
61
- </DropdownComponent>
62
- </DropDrawerContext.Provider>
63
- );
64
- }
65
-
66
- function DropDrawerTrigger({
67
- className,
68
- children,
69
- ...props
70
- }:
71
- | React.ComponentProps<typeof DrawerTrigger>
72
- | React.ComponentProps<typeof DropdownMenuTrigger>) {
73
- const { isMobile } = useDropDrawerContext();
74
- const TriggerComponent = isMobile ? DrawerTrigger : DropdownMenuTrigger;
75
-
76
- return (
77
- <TriggerComponent
78
- data-slot="drop-drawer-trigger"
79
- className={className}
80
- {...props}
81
- >
82
- {children}
83
- </TriggerComponent>
84
- );
85
- }
86
-
87
- function DropDrawerContent({
88
- className,
89
- children,
90
- ...props
91
- }:
92
- | React.ComponentProps<typeof DrawerContent>
93
- | React.ComponentProps<typeof DropdownMenuContent>) {
94
- const { isMobile } = useDropDrawerContext();
95
- const [activeSubmenu, setActiveSubmenu] = React.useState<string | null>(null);
96
- const [submenuTitle, setSubmenuTitle] = React.useState<string | null>(null);
97
- const [submenuStack, setSubmenuStack] = React.useState<
98
- { id: string; title: string }[]
99
- >([]);
100
- // Add animation direction state
101
- const [animationDirection, setAnimationDirection] = React.useState<
102
- "forward" | "backward"
103
- >("forward");
104
-
105
- // Create a ref to store submenu content by ID
106
- const submenuContentRef = React.useRef<Map<string, React.ReactNode[]>>(
107
- new Map(),
108
- );
109
-
110
- // Function to navigate to a submenu
111
- const navigateToSubmenu = React.useCallback((id: string, title: string) => {
112
- // Set animation direction to forward when navigating to a submenu
113
- setAnimationDirection("forward");
114
- setActiveSubmenu(id);
115
- setSubmenuTitle(title);
116
- setSubmenuStack((prev) => [...prev, { id, title }]);
117
- }, []);
118
-
119
- // Function to go back to previous menu
120
- const goBack = React.useCallback(() => {
121
- // Set animation direction to backward when going back
122
- setAnimationDirection("backward");
123
-
124
- if (submenuStack.length <= 1) {
125
- // If we're at the first level, go back to main menu
126
- setActiveSubmenu(null);
127
- setSubmenuTitle(null);
128
- setSubmenuStack([]);
129
- } else {
130
- // Go back to previous submenu
131
- const newStack = [...submenuStack];
132
- newStack.pop(); // Remove current
133
- const previous = newStack[newStack.length - 1];
134
- setActiveSubmenu(previous.id);
135
- setSubmenuTitle(previous.title);
136
- setSubmenuStack(newStack);
137
- }
138
- }, [submenuStack]);
139
-
140
- // Function to register submenu content
141
- const registerSubmenuContent = React.useCallback(
142
- (id: string, content: React.ReactNode[]) => {
143
- submenuContentRef.current.set(id, content);
144
- },
145
- [],
146
- );
147
-
148
- // Function to extract submenu content
149
- const extractSubmenuContent = React.useCallback(
150
- (elements: React.ReactNode, targetId: string): React.ReactNode[] => {
151
- const result: React.ReactNode[] = [];
152
-
153
- // Recursive function to search through all children
154
- const findSubmenuContent = (node: React.ReactNode) => {
155
- // Skip if not a valid element
156
- if (!React.isValidElement(node)) return;
157
-
158
- const element = node as React.ReactElement;
159
- // Use a more specific type to avoid 'any'
160
- const props = element.props as {
161
- id?: string;
162
- "data-submenu-id"?: string;
163
- children?: React.ReactNode;
164
- };
165
-
166
- // Check if this is a DropDrawerSub
167
- if (element.type === DropDrawerSub) {
168
- // Get all possible ID values
169
- const elementId = props.id;
170
- const dataSubmenuId = props["data-submenu-id"];
171
-
172
- // If this is the submenu we're looking for
173
- if (elementId === targetId || dataSubmenuId === targetId) {
174
- // Find the SubContent within this Sub
175
- if (props.children) {
176
- React.Children.forEach(props.children, (child) => {
177
- if (
178
- React.isValidElement(child) &&
179
- child.type === DropDrawerSubContent
180
- ) {
181
- // Add all children of the SubContent to the result
182
- const subContentProps = child.props as {
183
- children?: React.ReactNode;
184
- };
185
- if (subContentProps.children) {
186
- React.Children.forEach(
187
- subContentProps.children,
188
- (contentChild) => {
189
- result.push(contentChild);
190
- },
191
- );
192
- }
193
- }
194
- });
195
- }
196
- return; // Found what we needed, no need to search deeper
197
- }
198
- }
199
-
200
- // If this element has children, search through them
201
- if (props.children) {
202
- if (Array.isArray(props.children)) {
203
- props.children.forEach((child: React.ReactNode) =>
204
- findSubmenuContent(child),
205
- );
206
- } else {
207
- findSubmenuContent(props.children);
208
- }
209
- }
210
- };
211
-
212
- // Start the search from the root elements
213
- if (Array.isArray(elements)) {
214
- elements.forEach((child) => findSubmenuContent(child));
215
- } else {
216
- findSubmenuContent(elements);
217
- }
218
-
219
- return result;
220
- },
221
- [],
222
- );
223
-
224
- // Get submenu content (either from cache or extract it)
225
- const getSubmenuContent = React.useCallback(
226
- (id: string) => {
227
- // Check if we have the content in our ref
228
- const cachedContent = submenuContentRef.current.get(id || "");
229
- if (cachedContent && cachedContent.length > 0) {
230
- return cachedContent;
231
- }
232
-
233
- // If not in cache, extract it
234
- const submenuContent = extractSubmenuContent(children, id);
235
-
236
- if (submenuContent.length === 0) {
237
- return [];
238
- }
239
-
240
- // Store in cache for future use
241
- if (id) {
242
- submenuContentRef.current.set(id, submenuContent);
243
- }
244
-
245
- return submenuContent;
246
- },
247
- [children, extractSubmenuContent],
248
- );
249
-
250
- // Animation variants for Framer Motion
251
- const variants = {
252
- enter: (direction: "forward" | "backward") => ({
253
- x: direction === "forward" ? "100%" : "-100%",
254
- opacity: 0,
255
- }),
256
- center: {
257
- x: 0,
258
- opacity: 1,
259
- },
260
- exit: (direction: "forward" | "backward") => ({
261
- x: direction === "forward" ? "-100%" : "100%",
262
- opacity: 0,
263
- }),
264
- };
265
-
266
- // Animation transition
267
- const transition = {
268
- duration: 0.3,
269
- ease: [0.25, 0.1, 0.25, 1.0], // cubic-bezier easing
270
- } satisfies Transition;
271
-
272
- if (isMobile) {
273
- return (
274
- <SubmenuContext.Provider
275
- value={{
276
- activeSubmenu,
277
- setActiveSubmenu: (id) => {
278
- if (id === null) {
279
- setActiveSubmenu(null);
280
- setSubmenuTitle(null);
281
- setSubmenuStack([]);
282
- }
283
- },
284
- submenuTitle,
285
- setSubmenuTitle,
286
- navigateToSubmenu,
287
- registerSubmenuContent,
288
- }}
289
- >
290
- <DrawerContent
291
- data-slot="drop-drawer-content"
292
- className={cn("max-h-[90vh]", className)}
293
- {...props}
294
- >
295
- {activeSubmenu ? (
296
- <>
297
- <DrawerHeader>
298
- <div className="flex items-center gap-2">
299
- <button
300
- onClick={goBack}
301
- className="hover:bg-neutral-100/50 rounded-full p-1 dark:hover:bg-neutral-800/50"
302
- >
303
- <ChevronLeftIcon className="h-5 w-5" />
304
- </button>
305
- <DrawerTitle>{submenuTitle || "Submenu"}</DrawerTitle>
306
- </div>
307
- </DrawerHeader>
308
- <div className="flex-1 relative overflow-y-auto max-h-[70vh]">
309
- {/* Use AnimatePresence to handle exit animations */}
310
- <AnimatePresence
311
- initial={false}
312
- mode="wait"
313
- custom={animationDirection}
314
- >
315
- <motion.div
316
- key={activeSubmenu || "main"}
317
- custom={animationDirection}
318
- variants={variants}
319
- initial="enter"
320
- animate="center"
321
- exit="exit"
322
- transition={transition}
323
- className="pb-6 space-y-1.5 w-full h-full"
324
- >
325
- {activeSubmenu
326
- ? getSubmenuContent(activeSubmenu)
327
- : children}
328
- </motion.div>
329
- </AnimatePresence>
330
- </div>
331
- </>
332
- ) : (
333
- <>
334
- <DrawerHeader className="sr-only">
335
- <DrawerTitle>Menu</DrawerTitle>
336
- </DrawerHeader>
337
- <div className="overflow-y-auto max-h-[70vh]">
338
- <AnimatePresence
339
- initial={false}
340
- mode="wait"
341
- custom={animationDirection}
342
- >
343
- <motion.div
344
- key="main-menu"
345
- custom={animationDirection}
346
- variants={variants}
347
- initial="enter"
348
- animate="center"
349
- exit="exit"
350
- transition={transition}
351
- className="pb-6 space-y-1.5 w-full"
352
- >
353
- {children}
354
- </motion.div>
355
- </AnimatePresence>
356
- </div>
357
- </>
358
- )}
359
- </DrawerContent>
360
- </SubmenuContext.Provider>
361
- );
362
- }
363
-
364
- return (
365
- <SubmenuContext.Provider
366
- value={{
367
- activeSubmenu,
368
- setActiveSubmenu,
369
- submenuTitle,
370
- setSubmenuTitle,
371
- registerSubmenuContent,
372
- }}
373
- >
374
- <DropdownMenuContent
375
- data-slot="drop-drawer-content"
376
- align="end"
377
- sideOffset={4}
378
- className={cn(
379
- "max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[220px] overflow-y-auto",
380
- className,
381
- )}
382
- {...props}
383
- >
384
- {children}
385
- </DropdownMenuContent>
386
- </SubmenuContext.Provider>
387
- );
388
- }
389
-
390
- function DropDrawerItem({
391
- className,
392
- children,
393
- onSelect,
394
- onClick,
395
- icon,
396
- variant = "default",
397
- inset,
398
- disabled,
399
- ...props
400
- }: React.ComponentProps<typeof DropdownMenuItem> & {
401
- icon?: React.ReactNode;
402
- }) {
403
- const { isMobile } = useDropDrawerContext();
404
-
405
- // Define hooks outside of conditionals to follow React rules
406
- // Check if this item is inside a group by looking at parent elements
407
- const isInGroup = React.useCallback(
408
- (element: HTMLElement | null): boolean => {
409
- if (!element) return false;
410
-
411
- // Check if any parent has a data-drop-drawer-group attribute
412
- let parent = element.parentElement;
413
- while (parent) {
414
- if (parent.hasAttribute("data-drop-drawer-group")) {
415
- return true;
416
- }
417
- parent = parent.parentElement;
418
- }
419
- return false;
420
- },
421
- [],
422
- );
423
-
424
- // Create a ref to check if the item is in a group
425
- const itemRef = React.useRef<HTMLDivElement>(null);
426
- const [isInsideGroup, setIsInsideGroup] = React.useState(false);
427
-
428
- React.useEffect(() => {
429
- // Only run this effect in mobile mode
430
- if (!isMobile) return;
431
-
432
- // Use a short timeout to ensure the DOM is fully rendered
433
- const timer = setTimeout(() => {
434
- if (itemRef.current) {
435
- setIsInsideGroup(isInGroup(itemRef.current));
436
- }
437
- }, 0);
438
-
439
- return () => clearTimeout(timer);
440
- }, [isInGroup, isMobile]);
441
-
442
- if (isMobile) {
443
- const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
444
- if (disabled) return;
445
- if (onClick) onClick(e);
446
- if (onSelect) onSelect(e as unknown as Event);
447
- };
448
-
449
- // Only wrap in DrawerClose if it's not a submenu item
450
- const content = (
451
- <div
452
- ref={itemRef}
453
- data-slot="drop-drawer-item"
454
- data-variant={variant}
455
- data-inset={inset}
456
- data-disabled={disabled}
457
- className={cn(
458
- "flex cursor-pointer items-center justify-between px-4 py-4",
459
- // Only apply margin, background and rounded corners if not in a group
460
- !isInsideGroup &&
461
- "bg-neutral-100 dark:bg-neutral-100 mx-2 my-1.5 rounded-md dark:bg-neutral-800 dark:dark:bg-neutral-800",
462
- // For items in a group, don't add background but add more padding
463
- isInsideGroup && "bg-transparent py-4",
464
- inset && "pl-8",
465
- variant === "destructive" &&
466
- "text-red-500 dark:text-red-500 dark:text-red-900 dark:dark:text-red-900",
467
- disabled && "pointer-events-none opacity-50",
468
- className,
469
- )}
470
- onClick={handleClick}
471
- aria-disabled={disabled}
472
- {...props}
473
- >
474
- <div className="flex items-center gap-2">{children}</div>
475
- {icon && <div className="flex-shrink-0">{icon}</div>}
476
- </div>
477
- );
478
-
479
- // Check if this is inside a submenu
480
- const isInSubmenu =
481
- (props as Record<string, unknown>)["data-parent-submenu-id"] ||
482
- (props as Record<string, unknown>)["data-parent-submenu"];
483
-
484
- if (isInSubmenu) {
485
- return content;
486
- }
487
-
488
- return <DrawerClose asChild>{content}</DrawerClose>;
489
- }
490
-
491
- return (
492
- <DropdownMenuItem
493
- data-slot="drop-drawer-item"
494
- data-variant={variant}
495
- data-inset={inset}
496
- className={className}
497
- onSelect={onSelect}
498
- onClick={onClick as React.MouseEventHandler<HTMLDivElement>}
499
- variant={variant}
500
- inset={inset}
501
- disabled={disabled}
502
- {...props}
503
- >
504
- <div className="flex w-full items-center justify-between">
505
- <div>{children}</div>
506
- {icon && <div>{icon}</div>}
507
- </div>
508
- </DropdownMenuItem>
509
- );
510
- }
511
-
512
- function DropDrawerSeparator({
513
- className,
514
- ...props
515
- }: React.ComponentProps<typeof DropdownMenuSeparator>) {
516
- const { isMobile } = useDropDrawerContext();
517
-
518
- // For mobile, render a simple divider
519
- if (isMobile) {
520
- return null;
521
- }
522
-
523
- // For desktop, use the standard dropdown separator
524
- return (
525
- <DropdownMenuSeparator
526
- data-slot="drop-drawer-separator"
527
- className={className}
528
- {...props}
529
- />
530
- );
531
- }
532
-
533
- function DropDrawerLabel({
534
- className,
535
- children,
536
- ...props
537
- }:
538
- | React.ComponentProps<typeof DropdownMenuLabel>
539
- | React.ComponentProps<typeof DrawerTitle>) {
540
- const { isMobile } = useDropDrawerContext();
541
-
542
- if (isMobile) {
543
- return (
544
- <DrawerHeader className="p-0">
545
- <DrawerTitle
546
- data-slot="drop-drawer-label"
547
- className={cn(
548
- "text-neutral-500 px-4 py-2 text-sm font-medium dark:text-neutral-400",
549
- className,
550
- )}
551
- {...props}
552
- >
553
- {children}
554
- </DrawerTitle>
555
- </DrawerHeader>
556
- );
557
- }
558
-
559
- return (
560
- <DropdownMenuLabel
561
- data-slot="drop-drawer-label"
562
- className={className}
563
- {...props}
564
- >
565
- {children}
566
- </DropdownMenuLabel>
567
- );
568
- }
569
-
570
- function DropDrawerFooter({
571
- className,
572
- children,
573
- ...props
574
- }: React.ComponentProps<typeof DrawerFooter> | React.ComponentProps<"div">) {
575
- const { isMobile } = useDropDrawerContext();
576
-
577
- if (isMobile) {
578
- return (
579
- <DrawerFooter
580
- data-slot="drop-drawer-footer"
581
- className={cn("p-4", className)}
582
- {...props}
583
- >
584
- {children}
585
- </DrawerFooter>
586
- );
587
- }
588
-
589
- // No direct equivalent in DropdownMenu, so we'll just render a div
590
- return (
591
- <div
592
- data-slot="drop-drawer-footer"
593
- className={cn("p-2", className)}
594
- {...props}
595
- >
596
- {children}
597
- </div>
598
- );
599
- }
600
-
601
- function DropDrawerGroup({
602
- className,
603
- children,
604
- ...props
605
- }: React.ComponentProps<"div"> & {
606
- children: React.ReactNode;
607
- }) {
608
- const { isMobile } = useDropDrawerContext();
609
-
610
- // Add separators between children on mobile
611
- const childrenWithSeparators = React.useMemo(() => {
612
- if (!isMobile) return children;
613
-
614
- const childArray = React.Children.toArray(children);
615
-
616
- // Filter out any existing separators
617
- const filteredChildren = childArray.filter(
618
- (child) =>
619
- React.isValidElement(child) && child.type !== DropDrawerSeparator,
620
- );
621
-
622
- // Add separators between items
623
- return filteredChildren.flatMap((child, index) => {
624
- if (index === filteredChildren.length - 1) return [child];
625
- return [
626
- child,
627
- <div
628
- key={`separator-${index}`}
629
- className="bg-neutral-200 h-px dark:bg-neutral-800"
630
- aria-hidden="true"
631
- />,
632
- ];
633
- });
634
- }, [children, isMobile]);
635
-
636
- if (isMobile) {
637
- return (
638
- <div
639
- data-drop-drawer-group
640
- data-slot="drop-drawer-group"
641
- role="group"
642
- className={cn(
643
- "bg-neutral-100 dark:bg-neutral-100 mx-2 my-3 overflow-hidden rounded-xl dark:bg-neutral-800 dark:dark:bg-neutral-800",
644
- className,
645
- )}
646
- {...props}
647
- >
648
- {childrenWithSeparators}
649
- </div>
650
- );
651
- }
652
-
653
- // On desktop, use a div with proper role and attributes
654
- return (
655
- <div
656
- data-drop-drawer-group
657
- data-slot="drop-drawer-group"
658
- role="group"
659
- className={className}
660
- {...props}
661
- >
662
- {children}
663
- </div>
664
- );
665
- }
666
-
667
- // Context for managing submenu state on mobile
668
- interface SubmenuContextType {
669
- activeSubmenu: string | null;
670
- setActiveSubmenu: (id: string | null) => void;
671
- submenuTitle: string | null;
672
- setSubmenuTitle: (title: string | null) => void;
673
- navigateToSubmenu?: (id: string, title: string) => void;
674
- registerSubmenuContent?: (id: string, content: React.ReactNode[]) => void;
675
- }
676
-
677
- const SubmenuContext = React.createContext<SubmenuContextType>({
678
- activeSubmenu: null,
679
- setActiveSubmenu: () => {},
680
- submenuTitle: null,
681
- setSubmenuTitle: () => {},
682
- navigateToSubmenu: undefined,
683
- registerSubmenuContent: undefined,
684
- });
685
-
686
- // Submenu components
687
- // Counter for generating simple numeric IDs
688
- let submenuIdCounter = 0;
689
-
690
- function DropDrawerSub({
691
- children,
692
- id,
693
- ...props
694
- }: React.ComponentProps<typeof DropdownMenuSub> & {
695
- id?: string;
696
- }) {
697
- const { isMobile } = useDropDrawerContext();
698
- const { registerSubmenuContent } = React.useContext(SubmenuContext);
699
-
700
- // Generate a simple numeric ID instead of using React.useId()
701
- const [generatedId] = React.useState(() => `submenu-${submenuIdCounter++}`);
702
- const submenuId = id || generatedId;
703
-
704
- // Extract submenu content to register with parent
705
- React.useEffect(() => {
706
- if (!registerSubmenuContent) return;
707
-
708
- // Find the SubContent within this Sub
709
- const contentItems: React.ReactNode[] = [];
710
- React.Children.forEach(children, (child) => {
711
- if (React.isValidElement(child) && child.type === DropDrawerSubContent) {
712
- // Add all children of the SubContent to the result
713
- React.Children.forEach(
714
- (child.props as { children?: React.ReactNode }).children,
715
- (contentChild) => {
716
- contentItems.push(contentChild);
717
- },
718
- );
719
- }
720
- });
721
-
722
- // Register the content with the parent
723
- if (contentItems.length > 0) {
724
- registerSubmenuContent(submenuId, contentItems);
725
- }
726
- }, [children, registerSubmenuContent, submenuId]);
727
-
728
- if (isMobile) {
729
- // For mobile, we'll use the context to manage submenu state
730
- // Process children to pass the submenu ID to the trigger and content
731
- const processedChildren = React.Children.map(children, (child) => {
732
- if (!React.isValidElement(child)) return child;
733
-
734
- if (child.type === DropDrawerSubTrigger) {
735
- return React.cloneElement(
736
- child as React.ReactElement,
737
- {
738
- ...(child.props as object),
739
- "data-parent-submenu-id": submenuId,
740
- "data-submenu-id": submenuId,
741
- // Use only data attributes, not custom props
742
- "data-parent-submenu": submenuId,
743
- } as React.HTMLAttributes<HTMLElement>,
744
- );
745
- }
746
-
747
- if (child.type === DropDrawerSubContent) {
748
- return React.cloneElement(
749
- child as React.ReactElement,
750
- {
751
- ...(child.props as object),
752
- "data-parent-submenu-id": submenuId,
753
- "data-submenu-id": submenuId,
754
- // Use only data attributes, not custom props
755
- "data-parent-submenu": submenuId,
756
- } as React.HTMLAttributes<HTMLElement>,
757
- );
758
- }
759
-
760
- return child;
761
- });
762
-
763
- return (
764
- <div
765
- data-slot="drop-drawer-sub"
766
- data-submenu-id={submenuId}
767
- id={submenuId}
768
- >
769
- {processedChildren}
770
- </div>
771
- );
772
- }
773
-
774
- // For desktop, pass the generated ID to the DropdownMenuSub
775
- return (
776
- <DropdownMenuSub
777
- data-slot="drop-drawer-sub"
778
- data-submenu-id={submenuId}
779
- // Don't pass id to DropdownMenuSub as it doesn't accept this prop
780
- {...props}
781
- >
782
- {children}
783
- </DropdownMenuSub>
784
- );
785
- }
786
-
787
- function DropDrawerSubTrigger({
788
- className,
789
- inset,
790
- children,
791
- ...props
792
- }: React.ComponentProps<typeof DropdownMenuSubTrigger> & {
793
- icon?: React.ReactNode;
794
- }) {
795
- const { isMobile } = useDropDrawerContext();
796
- const { navigateToSubmenu } = React.useContext(SubmenuContext);
797
-
798
- // Define hooks outside of conditionals to follow React rules
799
- // Check if this item is inside a group by looking at parent elements
800
- const isInGroup = React.useCallback(
801
- (element: HTMLElement | null): boolean => {
802
- if (!element) return false;
803
-
804
- // Check if any parent has a data-drop-drawer-group attribute
805
- let parent = element.parentElement;
806
- while (parent) {
807
- if (parent.hasAttribute("data-drop-drawer-group")) {
808
- return true;
809
- }
810
- parent = parent.parentElement;
811
- }
812
- return false;
813
- },
814
- [],
815
- );
816
-
817
- // Create a ref to check if the item is in a group
818
- const itemRef = React.useRef<HTMLDivElement>(null);
819
- const [isInsideGroup, setIsInsideGroup] = React.useState(false);
820
-
821
- React.useEffect(() => {
822
- // Only run this effect in mobile mode
823
- if (!isMobile) return;
824
-
825
- // Use a short timeout to ensure the DOM is fully rendered
826
- const timer = setTimeout(() => {
827
- if (itemRef.current) {
828
- setIsInsideGroup(isInGroup(itemRef.current));
829
- }
830
- }, 0);
831
-
832
- return () => clearTimeout(timer);
833
- }, [isInGroup, isMobile]);
834
-
835
- if (isMobile) {
836
- // Find the parent submenu ID
837
- const handleClick = (e: React.MouseEvent) => {
838
- e.preventDefault();
839
- e.stopPropagation();
840
-
841
- // Get the closest parent with data-submenu-id attribute
842
- const element = e.currentTarget as HTMLElement;
843
- let submenuId: string | null = null;
844
-
845
- // First check if the element itself has the data attribute
846
- if (element.closest("[data-submenu-id]")) {
847
- const closestElement = element.closest("[data-submenu-id]");
848
- const id = closestElement?.getAttribute("data-submenu-id");
849
- if (id) {
850
- submenuId = id;
851
- }
852
- }
853
-
854
- // If not found, try props
855
- if (!submenuId) {
856
- submenuId =
857
- ((props as Record<string, unknown>)[
858
- "data-parent-submenu-id"
859
- ] as string) ||
860
- ((props as Record<string, unknown>)["data-parent-submenu"] as string);
861
- }
862
-
863
- if (!submenuId) {
864
- return;
865
- }
866
-
867
- // Get the title
868
- const title = typeof children === "string" ? children : "Submenu";
869
-
870
- // Navigate to the submenu
871
- if (navigateToSubmenu) {
872
- navigateToSubmenu(submenuId, title);
873
- }
874
- };
875
-
876
- // Combine onClick handlers
877
- const combinedOnClick = (e: React.MouseEvent) => {
878
- // Call the original onClick if provided
879
- const typedProps = props as Record<string, unknown>;
880
- if (typedProps["onClick"]) {
881
- const originalOnClick = typedProps[
882
- "onClick"
883
- ] as React.MouseEventHandler<HTMLDivElement>;
884
- originalOnClick(e as React.MouseEvent<HTMLDivElement>);
885
- }
886
-
887
- // Call our navigation handler
888
- handleClick(e);
889
- };
890
-
891
- // Remove onClick from props to avoid duplicate handlers
892
- const { ...restProps } = props as Record<string, unknown>;
893
-
894
- // Don't wrap in DrawerClose for submenu triggers
895
- return (
896
- <div
897
- ref={itemRef}
898
- data-slot="drop-drawer-sub-trigger"
899
- data-inset={inset}
900
- className={cn(
901
- "flex cursor-pointer items-center justify-between px-4 py-4",
902
- // Only apply margin, background and rounded corners if not in a group
903
- !isInsideGroup &&
904
- "bg-neutral-100 dark:bg-neutral-100 mx-2 my-1.5 rounded-md dark:bg-neutral-800 dark:dark:bg-neutral-800",
905
- // For items in a group, don't add background but add more padding
906
- isInsideGroup && "bg-transparent py-4",
907
- inset && "pl-8",
908
- className,
909
- )}
910
- onClick={combinedOnClick}
911
- {...restProps}
912
- >
913
- <div className="flex items-center gap-2">{children}</div>
914
- <ChevronRightIcon className="h-5 w-5" />
915
- </div>
916
- );
917
- }
918
-
919
- return (
920
- <DropdownMenuSubTrigger
921
- data-slot="drop-drawer-sub-trigger"
922
- data-inset={inset}
923
- className={className}
924
- inset={inset}
925
- {...props}
926
- >
927
- {children}
928
- </DropdownMenuSubTrigger>
929
- );
930
- }
931
-
932
- function DropDrawerSubContent({
933
- className,
934
- sideOffset = 4,
935
- children,
936
- ...props
937
- }: React.ComponentProps<typeof DropdownMenuSubContent>) {
938
- const { isMobile } = useDropDrawerContext();
939
-
940
- if (isMobile) {
941
- // For mobile, we don't render the content directly
942
- // It will be rendered by the DropDrawerContent component when active
943
- return null;
944
- }
945
-
946
- return (
947
- <DropdownMenuSubContent
948
- data-slot="drop-drawer-sub-content"
949
- sideOffset={sideOffset}
950
- className={cn(
951
- "z-50 min-w-[8rem] overflow-hidden rounded-md border border-neutral-200 p-1 shadow-lg dark:border-neutral-800",
952
- className,
953
- )}
954
- {...props}
955
- >
956
- {children}
957
- </DropdownMenuSubContent>
958
- );
959
- }
960
-
961
- export {
962
- DropDrawer,
963
- DropDrawerContent,
964
- DropDrawerFooter,
965
- DropDrawerGroup,
966
- DropDrawerItem,
967
- DropDrawerLabel,
968
- DropDrawerSeparator,
969
- DropDrawerSub,
970
- DropDrawerSubContent,
971
- DropDrawerSubTrigger,
972
- DropDrawerTrigger,
973
- };
1
+ "use client";
2
+
3
+ import { AnimatePresence, motion, Transition } from "motion/react";
4
+ import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
5
+ import * as React from "react";
6
+
7
+ import {
8
+ Drawer,
9
+ DrawerClose,
10
+ DrawerContent,
11
+ DrawerFooter,
12
+ DrawerHeader,
13
+ DrawerTitle,
14
+ DrawerTrigger,
15
+ } from "@/components/ui/drawer";
16
+ import {
17
+ DropdownMenu,
18
+ DropdownMenuContent,
19
+ DropdownMenuItem,
20
+ DropdownMenuLabel,
21
+ DropdownMenuSeparator,
22
+ DropdownMenuSub,
23
+ DropdownMenuSubContent,
24
+ DropdownMenuSubTrigger,
25
+ DropdownMenuTrigger,
26
+ } from "@/components/ui/dropdown-menu";
27
+ import { useIsMobile } from "@/hooks/useIsMobile";
28
+ import { cn } from "@/lib/utils";
29
+
30
+ const DropDrawerContext = React.createContext<{ isMobile: boolean }>({
31
+ isMobile: false,
32
+ });
33
+
34
+ const useDropDrawerContext = () => {
35
+ const context = React.useContext(DropDrawerContext);
36
+ if (!context) {
37
+ throw new Error(
38
+ "DropDrawer components cannot be rendered outside the Context",
39
+ );
40
+ }
41
+ return context;
42
+ };
43
+
44
+ function DropDrawer({
45
+ children,
46
+ ...props
47
+ }:
48
+ | React.ComponentProps<typeof Drawer>
49
+ | React.ComponentProps<typeof DropdownMenu>) {
50
+ const isMobile = useIsMobile();
51
+ const DropdownComponent = isMobile ? Drawer : DropdownMenu;
52
+
53
+ return (
54
+ <DropDrawerContext.Provider value={{ isMobile }}>
55
+ <DropdownComponent
56
+ data-slot="drop-drawer"
57
+ {...(isMobile && { autoFocus: true })}
58
+ {...props}
59
+ >
60
+ {children}
61
+ </DropdownComponent>
62
+ </DropDrawerContext.Provider>
63
+ );
64
+ }
65
+
66
+ function DropDrawerTrigger({
67
+ className,
68
+ children,
69
+ ...props
70
+ }:
71
+ | React.ComponentProps<typeof DrawerTrigger>
72
+ | React.ComponentProps<typeof DropdownMenuTrigger>) {
73
+ const { isMobile } = useDropDrawerContext();
74
+ const TriggerComponent = isMobile ? DrawerTrigger : DropdownMenuTrigger;
75
+
76
+ return (
77
+ <TriggerComponent
78
+ data-slot="drop-drawer-trigger"
79
+ className={className}
80
+ {...props}
81
+ >
82
+ {children}
83
+ </TriggerComponent>
84
+ );
85
+ }
86
+
87
+ function DropDrawerContent({
88
+ className,
89
+ children,
90
+ ...props
91
+ }:
92
+ | React.ComponentProps<typeof DrawerContent>
93
+ | React.ComponentProps<typeof DropdownMenuContent>) {
94
+ const { isMobile } = useDropDrawerContext();
95
+ const [activeSubmenu, setActiveSubmenu] = React.useState<string | null>(null);
96
+ const [submenuTitle, setSubmenuTitle] = React.useState<string | null>(null);
97
+ const [submenuStack, setSubmenuStack] = React.useState<
98
+ { id: string; title: string }[]
99
+ >([]);
100
+ // Add animation direction state
101
+ const [animationDirection, setAnimationDirection] = React.useState<
102
+ "forward" | "backward"
103
+ >("forward");
104
+
105
+ // Create a ref to store submenu content by ID
106
+ const submenuContentRef = React.useRef<Map<string, React.ReactNode[]>>(
107
+ new Map(),
108
+ );
109
+
110
+ // Function to navigate to a submenu
111
+ const navigateToSubmenu = React.useCallback((id: string, title: string) => {
112
+ // Set animation direction to forward when navigating to a submenu
113
+ setAnimationDirection("forward");
114
+ setActiveSubmenu(id);
115
+ setSubmenuTitle(title);
116
+ setSubmenuStack((prev) => [...prev, { id, title }]);
117
+ }, []);
118
+
119
+ // Function to go back to previous menu
120
+ const goBack = React.useCallback(() => {
121
+ // Set animation direction to backward when going back
122
+ setAnimationDirection("backward");
123
+
124
+ if (submenuStack.length <= 1) {
125
+ // If we're at the first level, go back to main menu
126
+ setActiveSubmenu(null);
127
+ setSubmenuTitle(null);
128
+ setSubmenuStack([]);
129
+ } else {
130
+ // Go back to previous submenu
131
+ const newStack = [...submenuStack];
132
+ newStack.pop(); // Remove current
133
+ const previous = newStack[newStack.length - 1];
134
+ setActiveSubmenu(previous.id);
135
+ setSubmenuTitle(previous.title);
136
+ setSubmenuStack(newStack);
137
+ }
138
+ }, [submenuStack]);
139
+
140
+ // Function to register submenu content
141
+ const registerSubmenuContent = React.useCallback(
142
+ (id: string, content: React.ReactNode[]) => {
143
+ submenuContentRef.current.set(id, content);
144
+ },
145
+ [],
146
+ );
147
+
148
+ // Function to extract submenu content
149
+ const extractSubmenuContent = React.useCallback(
150
+ (elements: React.ReactNode, targetId: string): React.ReactNode[] => {
151
+ const result: React.ReactNode[] = [];
152
+
153
+ // Recursive function to search through all children
154
+ const findSubmenuContent = (node: React.ReactNode) => {
155
+ // Skip if not a valid element
156
+ if (!React.isValidElement(node)) return;
157
+
158
+ const element = node as React.ReactElement;
159
+ // Use a more specific type to avoid 'any'
160
+ const props = element.props as {
161
+ id?: string;
162
+ "data-submenu-id"?: string;
163
+ children?: React.ReactNode;
164
+ };
165
+
166
+ // Check if this is a DropDrawerSub
167
+ if (element.type === DropDrawerSub) {
168
+ // Get all possible ID values
169
+ const elementId = props.id;
170
+ const dataSubmenuId = props["data-submenu-id"];
171
+
172
+ // If this is the submenu we're looking for
173
+ if (elementId === targetId || dataSubmenuId === targetId) {
174
+ // Find the SubContent within this Sub
175
+ if (props.children) {
176
+ React.Children.forEach(props.children, (child) => {
177
+ if (
178
+ React.isValidElement(child) &&
179
+ child.type === DropDrawerSubContent
180
+ ) {
181
+ // Add all children of the SubContent to the result
182
+ const subContentProps = child.props as {
183
+ children?: React.ReactNode;
184
+ };
185
+ if (subContentProps.children) {
186
+ React.Children.forEach(
187
+ subContentProps.children,
188
+ (contentChild) => {
189
+ result.push(contentChild);
190
+ },
191
+ );
192
+ }
193
+ }
194
+ });
195
+ }
196
+ return; // Found what we needed, no need to search deeper
197
+ }
198
+ }
199
+
200
+ // If this element has children, search through them
201
+ if (props.children) {
202
+ if (Array.isArray(props.children)) {
203
+ props.children.forEach((child: React.ReactNode) =>
204
+ findSubmenuContent(child),
205
+ );
206
+ } else {
207
+ findSubmenuContent(props.children);
208
+ }
209
+ }
210
+ };
211
+
212
+ // Start the search from the root elements
213
+ if (Array.isArray(elements)) {
214
+ elements.forEach((child) => findSubmenuContent(child));
215
+ } else {
216
+ findSubmenuContent(elements);
217
+ }
218
+
219
+ return result;
220
+ },
221
+ [],
222
+ );
223
+
224
+ // Get submenu content (either from cache or extract it)
225
+ const getSubmenuContent = React.useCallback(
226
+ (id: string) => {
227
+ // Check if we have the content in our ref
228
+ const cachedContent = submenuContentRef.current.get(id || "");
229
+ if (cachedContent && cachedContent.length > 0) {
230
+ return cachedContent;
231
+ }
232
+
233
+ // If not in cache, extract it
234
+ const submenuContent = extractSubmenuContent(children, id);
235
+
236
+ if (submenuContent.length === 0) {
237
+ return [];
238
+ }
239
+
240
+ // Store in cache for future use
241
+ if (id) {
242
+ submenuContentRef.current.set(id, submenuContent);
243
+ }
244
+
245
+ return submenuContent;
246
+ },
247
+ [children, extractSubmenuContent],
248
+ );
249
+
250
+ // Animation variants for Framer Motion
251
+ const variants = {
252
+ enter: (direction: "forward" | "backward") => ({
253
+ x: direction === "forward" ? "100%" : "-100%",
254
+ opacity: 0,
255
+ }),
256
+ center: {
257
+ x: 0,
258
+ opacity: 1,
259
+ },
260
+ exit: (direction: "forward" | "backward") => ({
261
+ x: direction === "forward" ? "-100%" : "100%",
262
+ opacity: 0,
263
+ }),
264
+ };
265
+
266
+ // Animation transition
267
+ const transition = {
268
+ duration: 0.3,
269
+ ease: [0.25, 0.1, 0.25, 1.0], // cubic-bezier easing
270
+ } satisfies Transition;
271
+
272
+ if (isMobile) {
273
+ return (
274
+ <SubmenuContext.Provider
275
+ value={{
276
+ activeSubmenu,
277
+ setActiveSubmenu: (id) => {
278
+ if (id === null) {
279
+ setActiveSubmenu(null);
280
+ setSubmenuTitle(null);
281
+ setSubmenuStack([]);
282
+ }
283
+ },
284
+ submenuTitle,
285
+ setSubmenuTitle,
286
+ navigateToSubmenu,
287
+ registerSubmenuContent,
288
+ }}
289
+ >
290
+ <DrawerContent
291
+ data-slot="drop-drawer-content"
292
+ className={cn("max-h-[90vh]", className)}
293
+ {...props}
294
+ >
295
+ {activeSubmenu ? (
296
+ <>
297
+ <DrawerHeader>
298
+ <div className="flex items-center gap-2">
299
+ <button
300
+ onClick={goBack}
301
+ className="hover:bg-neutral-100/50 rounded-full p-1 dark:hover:bg-neutral-800/50"
302
+ >
303
+ <ChevronLeftIcon className="h-5 w-5" />
304
+ </button>
305
+ <DrawerTitle>{submenuTitle || "Submenu"}</DrawerTitle>
306
+ </div>
307
+ </DrawerHeader>
308
+ <div className="flex-1 relative overflow-y-auto max-h-[70vh]">
309
+ {/* Use AnimatePresence to handle exit animations */}
310
+ <AnimatePresence
311
+ initial={false}
312
+ mode="wait"
313
+ custom={animationDirection}
314
+ >
315
+ <motion.div
316
+ key={activeSubmenu || "main"}
317
+ custom={animationDirection}
318
+ variants={variants}
319
+ initial="enter"
320
+ animate="center"
321
+ exit="exit"
322
+ transition={transition}
323
+ className="pb-6 space-y-1.5 w-full h-full"
324
+ >
325
+ {activeSubmenu
326
+ ? getSubmenuContent(activeSubmenu)
327
+ : children}
328
+ </motion.div>
329
+ </AnimatePresence>
330
+ </div>
331
+ </>
332
+ ) : (
333
+ <>
334
+ <DrawerHeader className="sr-only">
335
+ <DrawerTitle>Menu</DrawerTitle>
336
+ </DrawerHeader>
337
+ <div className="overflow-y-auto max-h-[70vh]">
338
+ <AnimatePresence
339
+ initial={false}
340
+ mode="wait"
341
+ custom={animationDirection}
342
+ >
343
+ <motion.div
344
+ key="main-menu"
345
+ custom={animationDirection}
346
+ variants={variants}
347
+ initial="enter"
348
+ animate="center"
349
+ exit="exit"
350
+ transition={transition}
351
+ className="pb-6 space-y-1.5 w-full"
352
+ >
353
+ {children}
354
+ </motion.div>
355
+ </AnimatePresence>
356
+ </div>
357
+ </>
358
+ )}
359
+ </DrawerContent>
360
+ </SubmenuContext.Provider>
361
+ );
362
+ }
363
+
364
+ return (
365
+ <SubmenuContext.Provider
366
+ value={{
367
+ activeSubmenu,
368
+ setActiveSubmenu,
369
+ submenuTitle,
370
+ setSubmenuTitle,
371
+ registerSubmenuContent,
372
+ }}
373
+ >
374
+ <DropdownMenuContent
375
+ data-slot="drop-drawer-content"
376
+ align="end"
377
+ sideOffset={4}
378
+ className={cn(
379
+ "max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[220px] overflow-y-auto",
380
+ className,
381
+ )}
382
+ {...props}
383
+ >
384
+ {children}
385
+ </DropdownMenuContent>
386
+ </SubmenuContext.Provider>
387
+ );
388
+ }
389
+
390
+ function DropDrawerItem({
391
+ className,
392
+ children,
393
+ onSelect,
394
+ onClick,
395
+ icon,
396
+ variant = "default",
397
+ inset,
398
+ disabled,
399
+ ...props
400
+ }: React.ComponentProps<typeof DropdownMenuItem> & {
401
+ icon?: React.ReactNode;
402
+ }) {
403
+ const { isMobile } = useDropDrawerContext();
404
+
405
+ // Define hooks outside of conditionals to follow React rules
406
+ // Check if this item is inside a group by looking at parent elements
407
+ const isInGroup = React.useCallback(
408
+ (element: HTMLElement | null): boolean => {
409
+ if (!element) return false;
410
+
411
+ // Check if any parent has a data-drop-drawer-group attribute
412
+ let parent = element.parentElement;
413
+ while (parent) {
414
+ if (parent.hasAttribute("data-drop-drawer-group")) {
415
+ return true;
416
+ }
417
+ parent = parent.parentElement;
418
+ }
419
+ return false;
420
+ },
421
+ [],
422
+ );
423
+
424
+ // Create a ref to check if the item is in a group
425
+ const itemRef = React.useRef<HTMLDivElement>(null);
426
+ const [isInsideGroup, setIsInsideGroup] = React.useState(false);
427
+
428
+ React.useEffect(() => {
429
+ // Only run this effect in mobile mode
430
+ if (!isMobile) return;
431
+
432
+ // Use a short timeout to ensure the DOM is fully rendered
433
+ const timer = setTimeout(() => {
434
+ if (itemRef.current) {
435
+ setIsInsideGroup(isInGroup(itemRef.current));
436
+ }
437
+ }, 0);
438
+
439
+ return () => clearTimeout(timer);
440
+ }, [isInGroup, isMobile]);
441
+
442
+ if (isMobile) {
443
+ const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
444
+ if (disabled) return;
445
+ if (onClick) onClick(e);
446
+ if (onSelect) onSelect(e as unknown as Event);
447
+ };
448
+
449
+ // Only wrap in DrawerClose if it's not a submenu item
450
+ const content = (
451
+ <div
452
+ ref={itemRef}
453
+ data-slot="drop-drawer-item"
454
+ data-variant={variant}
455
+ data-inset={inset}
456
+ data-disabled={disabled}
457
+ className={cn(
458
+ "flex cursor-pointer items-center justify-between px-4 py-4",
459
+ // Only apply margin, background and rounded corners if not in a group
460
+ !isInsideGroup &&
461
+ "bg-neutral-100 dark:bg-neutral-100 mx-2 my-1.5 rounded-md dark:bg-neutral-800 dark:dark:bg-neutral-800",
462
+ // For items in a group, don't add background but add more padding
463
+ isInsideGroup && "bg-transparent py-4",
464
+ inset && "pl-8",
465
+ variant === "destructive" &&
466
+ "text-red-500 dark:text-red-500 dark:text-red-900 dark:dark:text-red-900",
467
+ disabled && "pointer-events-none opacity-50",
468
+ className,
469
+ )}
470
+ onClick={handleClick}
471
+ aria-disabled={disabled}
472
+ {...props}
473
+ >
474
+ <div className="flex items-center gap-2">{children}</div>
475
+ {icon && <div className="flex-shrink-0">{icon}</div>}
476
+ </div>
477
+ );
478
+
479
+ // Check if this is inside a submenu
480
+ const isInSubmenu =
481
+ (props as Record<string, unknown>)["data-parent-submenu-id"] ||
482
+ (props as Record<string, unknown>)["data-parent-submenu"];
483
+
484
+ if (isInSubmenu) {
485
+ return content;
486
+ }
487
+
488
+ return <DrawerClose asChild>{content}</DrawerClose>;
489
+ }
490
+
491
+ return (
492
+ <DropdownMenuItem
493
+ data-slot="drop-drawer-item"
494
+ data-variant={variant}
495
+ data-inset={inset}
496
+ className={className}
497
+ onSelect={onSelect}
498
+ onClick={onClick as React.MouseEventHandler<HTMLDivElement>}
499
+ variant={variant}
500
+ inset={inset}
501
+ disabled={disabled}
502
+ {...props}
503
+ >
504
+ <div className="flex w-full items-center justify-between">
505
+ <div>{children}</div>
506
+ {icon && <div>{icon}</div>}
507
+ </div>
508
+ </DropdownMenuItem>
509
+ );
510
+ }
511
+
512
+ function DropDrawerSeparator({
513
+ className,
514
+ ...props
515
+ }: React.ComponentProps<typeof DropdownMenuSeparator>) {
516
+ const { isMobile } = useDropDrawerContext();
517
+
518
+ // For mobile, render a simple divider
519
+ if (isMobile) {
520
+ return null;
521
+ }
522
+
523
+ // For desktop, use the standard dropdown separator
524
+ return (
525
+ <DropdownMenuSeparator
526
+ data-slot="drop-drawer-separator"
527
+ className={className}
528
+ {...props}
529
+ />
530
+ );
531
+ }
532
+
533
+ function DropDrawerLabel({
534
+ className,
535
+ children,
536
+ ...props
537
+ }:
538
+ | React.ComponentProps<typeof DropdownMenuLabel>
539
+ | React.ComponentProps<typeof DrawerTitle>) {
540
+ const { isMobile } = useDropDrawerContext();
541
+
542
+ if (isMobile) {
543
+ return (
544
+ <DrawerHeader className="p-0">
545
+ <DrawerTitle
546
+ data-slot="drop-drawer-label"
547
+ className={cn(
548
+ "text-neutral-500 px-4 py-2 text-sm font-medium dark:text-neutral-400",
549
+ className,
550
+ )}
551
+ {...props}
552
+ >
553
+ {children}
554
+ </DrawerTitle>
555
+ </DrawerHeader>
556
+ );
557
+ }
558
+
559
+ return (
560
+ <DropdownMenuLabel
561
+ data-slot="drop-drawer-label"
562
+ className={className}
563
+ {...props}
564
+ >
565
+ {children}
566
+ </DropdownMenuLabel>
567
+ );
568
+ }
569
+
570
+ function DropDrawerFooter({
571
+ className,
572
+ children,
573
+ ...props
574
+ }: React.ComponentProps<typeof DrawerFooter> | React.ComponentProps<"div">) {
575
+ const { isMobile } = useDropDrawerContext();
576
+
577
+ if (isMobile) {
578
+ return (
579
+ <DrawerFooter
580
+ data-slot="drop-drawer-footer"
581
+ className={cn("p-4", className)}
582
+ {...props}
583
+ >
584
+ {children}
585
+ </DrawerFooter>
586
+ );
587
+ }
588
+
589
+ // No direct equivalent in DropdownMenu, so we'll just render a div
590
+ return (
591
+ <div
592
+ data-slot="drop-drawer-footer"
593
+ className={cn("p-2", className)}
594
+ {...props}
595
+ >
596
+ {children}
597
+ </div>
598
+ );
599
+ }
600
+
601
+ function DropDrawerGroup({
602
+ className,
603
+ children,
604
+ ...props
605
+ }: React.ComponentProps<"div"> & {
606
+ children: React.ReactNode;
607
+ }) {
608
+ const { isMobile } = useDropDrawerContext();
609
+
610
+ // Add separators between children on mobile
611
+ const childrenWithSeparators = React.useMemo(() => {
612
+ if (!isMobile) return children;
613
+
614
+ const childArray = React.Children.toArray(children);
615
+
616
+ // Filter out any existing separators
617
+ const filteredChildren = childArray.filter(
618
+ (child) =>
619
+ React.isValidElement(child) && child.type !== DropDrawerSeparator,
620
+ );
621
+
622
+ // Add separators between items
623
+ return filteredChildren.flatMap((child, index) => {
624
+ if (index === filteredChildren.length - 1) return [child];
625
+ return [
626
+ child,
627
+ <div
628
+ key={`separator-${index}`}
629
+ className="bg-neutral-200 h-px dark:bg-neutral-800"
630
+ aria-hidden="true"
631
+ />,
632
+ ];
633
+ });
634
+ }, [children, isMobile]);
635
+
636
+ if (isMobile) {
637
+ return (
638
+ <div
639
+ data-drop-drawer-group
640
+ data-slot="drop-drawer-group"
641
+ role="group"
642
+ className={cn(
643
+ "bg-neutral-100 dark:bg-neutral-100 mx-2 my-3 overflow-hidden rounded-xl dark:bg-neutral-800 dark:dark:bg-neutral-800",
644
+ className,
645
+ )}
646
+ {...props}
647
+ >
648
+ {childrenWithSeparators}
649
+ </div>
650
+ );
651
+ }
652
+
653
+ // On desktop, use a div with proper role and attributes
654
+ return (
655
+ <div
656
+ data-drop-drawer-group
657
+ data-slot="drop-drawer-group"
658
+ role="group"
659
+ className={className}
660
+ {...props}
661
+ >
662
+ {children}
663
+ </div>
664
+ );
665
+ }
666
+
667
+ // Context for managing submenu state on mobile
668
+ interface SubmenuContextType {
669
+ activeSubmenu: string | null;
670
+ setActiveSubmenu: (id: string | null) => void;
671
+ submenuTitle: string | null;
672
+ setSubmenuTitle: (title: string | null) => void;
673
+ navigateToSubmenu?: (id: string, title: string) => void;
674
+ registerSubmenuContent?: (id: string, content: React.ReactNode[]) => void;
675
+ }
676
+
677
+ const SubmenuContext = React.createContext<SubmenuContextType>({
678
+ activeSubmenu: null,
679
+ setActiveSubmenu: () => {},
680
+ submenuTitle: null,
681
+ setSubmenuTitle: () => {},
682
+ navigateToSubmenu: undefined,
683
+ registerSubmenuContent: undefined,
684
+ });
685
+
686
+ // Submenu components
687
+ // Counter for generating simple numeric IDs
688
+ let submenuIdCounter = 0;
689
+
690
+ function DropDrawerSub({
691
+ children,
692
+ id,
693
+ ...props
694
+ }: React.ComponentProps<typeof DropdownMenuSub> & {
695
+ id?: string;
696
+ }) {
697
+ const { isMobile } = useDropDrawerContext();
698
+ const { registerSubmenuContent } = React.useContext(SubmenuContext);
699
+
700
+ // Generate a simple numeric ID instead of using React.useId()
701
+ const [generatedId] = React.useState(() => `submenu-${submenuIdCounter++}`);
702
+ const submenuId = id || generatedId;
703
+
704
+ // Extract submenu content to register with parent
705
+ React.useEffect(() => {
706
+ if (!registerSubmenuContent) return;
707
+
708
+ // Find the SubContent within this Sub
709
+ const contentItems: React.ReactNode[] = [];
710
+ React.Children.forEach(children, (child) => {
711
+ if (React.isValidElement(child) && child.type === DropDrawerSubContent) {
712
+ // Add all children of the SubContent to the result
713
+ React.Children.forEach(
714
+ (child.props as { children?: React.ReactNode }).children,
715
+ (contentChild) => {
716
+ contentItems.push(contentChild);
717
+ },
718
+ );
719
+ }
720
+ });
721
+
722
+ // Register the content with the parent
723
+ if (contentItems.length > 0) {
724
+ registerSubmenuContent(submenuId, contentItems);
725
+ }
726
+ }, [children, registerSubmenuContent, submenuId]);
727
+
728
+ if (isMobile) {
729
+ // For mobile, we'll use the context to manage submenu state
730
+ // Process children to pass the submenu ID to the trigger and content
731
+ const processedChildren = React.Children.map(children, (child) => {
732
+ if (!React.isValidElement(child)) return child;
733
+
734
+ if (child.type === DropDrawerSubTrigger) {
735
+ return React.cloneElement(
736
+ child as React.ReactElement,
737
+ {
738
+ ...(child.props as object),
739
+ "data-parent-submenu-id": submenuId,
740
+ "data-submenu-id": submenuId,
741
+ // Use only data attributes, not custom props
742
+ "data-parent-submenu": submenuId,
743
+ } as React.HTMLAttributes<HTMLElement>,
744
+ );
745
+ }
746
+
747
+ if (child.type === DropDrawerSubContent) {
748
+ return React.cloneElement(
749
+ child as React.ReactElement,
750
+ {
751
+ ...(child.props as object),
752
+ "data-parent-submenu-id": submenuId,
753
+ "data-submenu-id": submenuId,
754
+ // Use only data attributes, not custom props
755
+ "data-parent-submenu": submenuId,
756
+ } as React.HTMLAttributes<HTMLElement>,
757
+ );
758
+ }
759
+
760
+ return child;
761
+ });
762
+
763
+ return (
764
+ <div
765
+ data-slot="drop-drawer-sub"
766
+ data-submenu-id={submenuId}
767
+ id={submenuId}
768
+ >
769
+ {processedChildren}
770
+ </div>
771
+ );
772
+ }
773
+
774
+ // For desktop, pass the generated ID to the DropdownMenuSub
775
+ return (
776
+ <DropdownMenuSub
777
+ data-slot="drop-drawer-sub"
778
+ data-submenu-id={submenuId}
779
+ // Don't pass id to DropdownMenuSub as it doesn't accept this prop
780
+ {...props}
781
+ >
782
+ {children}
783
+ </DropdownMenuSub>
784
+ );
785
+ }
786
+
787
+ function DropDrawerSubTrigger({
788
+ className,
789
+ inset,
790
+ children,
791
+ ...props
792
+ }: React.ComponentProps<typeof DropdownMenuSubTrigger> & {
793
+ icon?: React.ReactNode;
794
+ }) {
795
+ const { isMobile } = useDropDrawerContext();
796
+ const { navigateToSubmenu } = React.useContext(SubmenuContext);
797
+
798
+ // Define hooks outside of conditionals to follow React rules
799
+ // Check if this item is inside a group by looking at parent elements
800
+ const isInGroup = React.useCallback(
801
+ (element: HTMLElement | null): boolean => {
802
+ if (!element) return false;
803
+
804
+ // Check if any parent has a data-drop-drawer-group attribute
805
+ let parent = element.parentElement;
806
+ while (parent) {
807
+ if (parent.hasAttribute("data-drop-drawer-group")) {
808
+ return true;
809
+ }
810
+ parent = parent.parentElement;
811
+ }
812
+ return false;
813
+ },
814
+ [],
815
+ );
816
+
817
+ // Create a ref to check if the item is in a group
818
+ const itemRef = React.useRef<HTMLDivElement>(null);
819
+ const [isInsideGroup, setIsInsideGroup] = React.useState(false);
820
+
821
+ React.useEffect(() => {
822
+ // Only run this effect in mobile mode
823
+ if (!isMobile) return;
824
+
825
+ // Use a short timeout to ensure the DOM is fully rendered
826
+ const timer = setTimeout(() => {
827
+ if (itemRef.current) {
828
+ setIsInsideGroup(isInGroup(itemRef.current));
829
+ }
830
+ }, 0);
831
+
832
+ return () => clearTimeout(timer);
833
+ }, [isInGroup, isMobile]);
834
+
835
+ if (isMobile) {
836
+ // Find the parent submenu ID
837
+ const handleClick = (e: React.MouseEvent) => {
838
+ e.preventDefault();
839
+ e.stopPropagation();
840
+
841
+ // Get the closest parent with data-submenu-id attribute
842
+ const element = e.currentTarget as HTMLElement;
843
+ let submenuId: string | null = null;
844
+
845
+ // First check if the element itself has the data attribute
846
+ if (element.closest("[data-submenu-id]")) {
847
+ const closestElement = element.closest("[data-submenu-id]");
848
+ const id = closestElement?.getAttribute("data-submenu-id");
849
+ if (id) {
850
+ submenuId = id;
851
+ }
852
+ }
853
+
854
+ // If not found, try props
855
+ if (!submenuId) {
856
+ submenuId =
857
+ ((props as Record<string, unknown>)[
858
+ "data-parent-submenu-id"
859
+ ] as string) ||
860
+ ((props as Record<string, unknown>)["data-parent-submenu"] as string);
861
+ }
862
+
863
+ if (!submenuId) {
864
+ return;
865
+ }
866
+
867
+ // Get the title
868
+ const title = typeof children === "string" ? children : "Submenu";
869
+
870
+ // Navigate to the submenu
871
+ if (navigateToSubmenu) {
872
+ navigateToSubmenu(submenuId, title);
873
+ }
874
+ };
875
+
876
+ // Combine onClick handlers
877
+ const combinedOnClick = (e: React.MouseEvent) => {
878
+ // Call the original onClick if provided
879
+ const typedProps = props as Record<string, unknown>;
880
+ if (typedProps["onClick"]) {
881
+ const originalOnClick = typedProps[
882
+ "onClick"
883
+ ] as React.MouseEventHandler<HTMLDivElement>;
884
+ originalOnClick(e as React.MouseEvent<HTMLDivElement>);
885
+ }
886
+
887
+ // Call our navigation handler
888
+ handleClick(e);
889
+ };
890
+
891
+ // Remove onClick from props to avoid duplicate handlers
892
+ const { ...restProps } = props as Record<string, unknown>;
893
+
894
+ // Don't wrap in DrawerClose for submenu triggers
895
+ return (
896
+ <div
897
+ ref={itemRef}
898
+ data-slot="drop-drawer-sub-trigger"
899
+ data-inset={inset}
900
+ className={cn(
901
+ "flex cursor-pointer items-center justify-between px-4 py-4",
902
+ // Only apply margin, background and rounded corners if not in a group
903
+ !isInsideGroup &&
904
+ "bg-neutral-100 dark:bg-neutral-100 mx-2 my-1.5 rounded-md dark:bg-neutral-800 dark:dark:bg-neutral-800",
905
+ // For items in a group, don't add background but add more padding
906
+ isInsideGroup && "bg-transparent py-4",
907
+ inset && "pl-8",
908
+ className,
909
+ )}
910
+ onClick={combinedOnClick}
911
+ {...restProps}
912
+ >
913
+ <div className="flex items-center gap-2">{children}</div>
914
+ <ChevronRightIcon className="h-5 w-5" />
915
+ </div>
916
+ );
917
+ }
918
+
919
+ return (
920
+ <DropdownMenuSubTrigger
921
+ data-slot="drop-drawer-sub-trigger"
922
+ data-inset={inset}
923
+ className={className}
924
+ inset={inset}
925
+ {...props}
926
+ >
927
+ {children}
928
+ </DropdownMenuSubTrigger>
929
+ );
930
+ }
931
+
932
+ function DropDrawerSubContent({
933
+ className,
934
+ sideOffset = 4,
935
+ children,
936
+ ...props
937
+ }: React.ComponentProps<typeof DropdownMenuSubContent>) {
938
+ const { isMobile } = useDropDrawerContext();
939
+
940
+ if (isMobile) {
941
+ // For mobile, we don't render the content directly
942
+ // It will be rendered by the DropDrawerContent component when active
943
+ return null;
944
+ }
945
+
946
+ return (
947
+ <DropdownMenuSubContent
948
+ data-slot="drop-drawer-sub-content"
949
+ sideOffset={sideOffset}
950
+ className={cn(
951
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border border-neutral-200 p-1 shadow-lg dark:border-neutral-800",
952
+ className,
953
+ )}
954
+ {...props}
955
+ >
956
+ {children}
957
+ </DropdownMenuSubContent>
958
+ );
959
+ }
960
+
961
+ export {
962
+ DropDrawer,
963
+ DropDrawerContent,
964
+ DropDrawerFooter,
965
+ DropDrawerGroup,
966
+ DropDrawerItem,
967
+ DropDrawerLabel,
968
+ DropDrawerSeparator,
969
+ DropDrawerSub,
970
+ DropDrawerSubContent,
971
+ DropDrawerSubTrigger,
972
+ DropDrawerTrigger,
973
+ };