forked from
slices.network/slices
Highly ambitious ATProtocol AppView service and sdks
1import { useState, useRef, useEffect, createContext, useContext, ReactNode } from "react";
2import { cn } from "../utils/cn.ts";
3
4interface DropdownContextValue {
5 isOpen: boolean;
6 setIsOpen: (isOpen: boolean) => void;
7}
8
9const DropdownContext = createContext<DropdownContextValue | null>(null);
10
11function useDropdownContext() {
12 const context = useContext(DropdownContext);
13 if (!context) {
14 throw new Error("Dropdown components must be used within a Dropdown");
15 }
16 return context;
17}
18
19interface DropdownProps {
20 children: ReactNode;
21}
22
23function DropdownRoot({ children }: DropdownProps) {
24 const [isOpen, setIsOpen] = useState(false);
25 const dropdownRef = useRef<HTMLDivElement>(null);
26
27 useEffect(() => {
28 const handleClickOutside = (event: MouseEvent) => {
29 if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
30 setIsOpen(false);
31 }
32 };
33
34 if (isOpen) {
35 document.addEventListener("mousedown", handleClickOutside);
36 }
37
38 return () => {
39 document.removeEventListener("mousedown", handleClickOutside);
40 };
41 }, [isOpen]);
42
43 return (
44 <DropdownContext.Provider value={{ isOpen, setIsOpen }}>
45 <div className="relative" ref={dropdownRef}>
46 {children}
47 </div>
48 </DropdownContext.Provider>
49 );
50}
51
52interface DropdownTriggerProps {
53 children: ReactNode;
54 className?: string;
55}
56
57function DropdownTrigger({ children, className }: DropdownTriggerProps) {
58 const { isOpen, setIsOpen } = useDropdownContext();
59
60 return (
61 <div onClick={() => setIsOpen(!isOpen)} className={className}>
62 {children}
63 </div>
64 );
65}
66
67interface DropdownContentProps {
68 children: ReactNode;
69 align?: "left" | "right";
70 className?: string;
71}
72
73function DropdownContent({ children, align = "right", className }: DropdownContentProps) {
74 const { isOpen, setIsOpen } = useDropdownContext();
75
76 if (!isOpen) return null;
77
78 return (
79 <div
80 className={cn(
81 "absolute mt-1 w-48 bg-zinc-900 border border-zinc-800 rounded shadow-lg z-20",
82 align === "left" ? "left-0" : "right-0",
83 className
84 )}
85 onClick={() => setIsOpen(false)}
86 >
87 {children}
88 </div>
89 );
90}
91
92interface DropdownItemProps {
93 onClick?: () => void;
94 children: ReactNode;
95 variant?: "default" | "danger";
96 className?: string;
97}
98
99function DropdownItem({ onClick, children, variant: _variant = "default", className }: DropdownItemProps) {
100 const variantClasses = "text-zinc-300 hover:bg-zinc-800";
101
102 return (
103 <button
104 type="button"
105 onClick={onClick}
106 className={cn(
107 "w-full flex items-center gap-2 px-4 py-2 text-xs transition-colors cursor-pointer",
108 variantClasses,
109 className
110 )}
111 >
112 {children}
113 </button>
114 );
115}
116
117export const Dropdown = Object.assign(DropdownRoot, {
118 Trigger: DropdownTrigger,
119 Content: DropdownContent,
120 Item: DropdownItem,
121});