apex-ui-kit 2.1.3 → 2.2.0

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.
@@ -0,0 +1,220 @@
1
+ import React, { useId, useRef, useState } from "react";
2
+ import { AnimatePresence, LayoutGroup, motion } from "framer-motion";
3
+ import { BarChart3, Bell, ChevronDown, FolderKanban, Home, Inbox, LifeBuoy, PanelLeftClose, PanelLeftOpen, Settings, Users } from "lucide-react";
4
+
5
+ /*
6
+ * Sidebar Nav: an app sidebar that collapses to an icon rail. The active
7
+ * pill slides between items, groups open and close, badges show counts, and
8
+ * the rail shows each name as a tooltip.
9
+ */
10
+
11
+ const DEFAULT_SECTIONS = [
12
+ {
13
+ title: "Main",
14
+ items: [
15
+ { id: "home", label: "Dashboard", icon: Home },
16
+ { id: "inbox", label: "Inbox", icon: Inbox, badge: 4 },
17
+ { id: "analytics", label: "Analytics", icon: BarChart3 },
18
+ {
19
+ id: "projects",
20
+ label: "Projects",
21
+ icon: FolderKanban,
22
+ children: [
23
+ { id: "apex", label: "ApexUI" },
24
+ { id: "website", label: "Website" },
25
+ { id: "mobile", label: "Mobile app" },
26
+ ],
27
+ },
28
+ { id: "team", label: "Team", icon: Users },
29
+ ],
30
+ },
31
+ {
32
+ title: "Other",
33
+ items: [
34
+ { id: "notifications", label: "Notifications", icon: Bell, badge: 12 },
35
+ { id: "settings", label: "Settings", icon: Settings },
36
+ { id: "help", label: "Help", icon: LifeBuoy },
37
+ ],
38
+ },
39
+ ];
40
+
41
+ const THEMES = {
42
+ dark: {
43
+ panel: "bg-neutral-950 text-white border-white/10",
44
+ muted: "text-white/45",
45
+ item: "text-white/70 hover:text-white hover:bg-white/[0.05]",
46
+ pill: "bg-white/[0.08]",
47
+ tip: "bg-white text-neutral-900",
48
+ },
49
+ light: {
50
+ panel: "bg-white text-neutral-900 border-black/10",
51
+ muted: "text-neutral-400",
52
+ item: "text-neutral-600 hover:text-neutral-900 hover:bg-black/[0.04]",
53
+ pill: "bg-black/[0.06]",
54
+ tip: "bg-neutral-900 text-white",
55
+ },
56
+ };
57
+
58
+ const SidebarNav = ({
59
+ sections = DEFAULT_SECTIONS,
60
+ brand = { name: "Apex", logo: null },
61
+ user = { name: "Mohan Dalei", email: "mohan@apex-ui.in", avatar: "https://ik.imagekit.io/ny6yinyut/apexui/assets/captainamerica.png?tr=f-auto,q-80,w-96" },
62
+ defaultActive = "home",
63
+ defaultCollapsed = false,
64
+ accentColor = "#a3e635",
65
+ theme = "dark",
66
+ onSelect,
67
+ className = "",
68
+ }) => {
69
+ const t = THEMES[theme] ?? THEMES.dark;
70
+ const pillId = useId();
71
+ const [collapsed, setCollapsed] = useState(defaultCollapsed);
72
+ const [active, setActive] = useState(defaultActive);
73
+ const [open, setOpen] = useState(() => new Set(["projects"]));
74
+ const [tip, setTip] = useState(null);
75
+ const asideRef = useRef(null);
76
+
77
+ const select = (item) => {
78
+ setActive(item.id);
79
+ onSelect?.(item);
80
+ };
81
+ const toggle = (id) =>
82
+ setOpen((prev) => {
83
+ const next = new Set(prev);
84
+ if (next.has(id)) next.delete(id);
85
+ else next.add(id);
86
+ return next;
87
+ });
88
+
89
+ // A render function (not a component) so rows keep their identity between renders.
90
+ const renderRow = (item, depth = 0) => {
91
+ const Icon = item.icon;
92
+ const hasChildren = Boolean(item.children?.length);
93
+ const isOpen = open.has(item.id);
94
+ const isActive = active === item.id || (hasChildren && collapsed && item.children.some((c) => c.id === active));
95
+ return (
96
+ <li key={item.id}>
97
+ <button
98
+ type="button"
99
+ onClick={() => (hasChildren && !collapsed ? toggle(item.id) : select(hasChildren ? item.children[0] : item))}
100
+ onMouseEnter={(e) => {
101
+ if (!collapsed) return;
102
+ const r = e.currentTarget.getBoundingClientRect();
103
+ setTip({ label: item.label, top: r.top + r.height / 2 - asideRef.current.getBoundingClientRect().top });
104
+ }}
105
+ onMouseLeave={() => setTip(null)}
106
+ aria-label={item.label}
107
+ aria-current={isActive ? "page" : undefined}
108
+ aria-expanded={hasChildren && !collapsed ? isOpen : undefined}
109
+ className={`relative flex h-9 w-full items-center gap-3 rounded-lg text-[0.86rem] font-medium transition-colors ${collapsed ? "justify-center px-0" : "px-2.5"} ${isActive ? "" : t.item}`}
110
+ style={{ paddingLeft: !collapsed && depth ? "2.6rem" : undefined }}
111
+ >
112
+ {isActive && (
113
+ <motion.span layoutId={`${pillId}-pill`} className={`absolute inset-0 rounded-lg ${t.pill}`} transition={{ type: "spring", stiffness: 500, damping: 38 }}>
114
+ <span className="absolute inset-y-2 left-0 w-[3px] rounded-full" style={{ background: accentColor }} />
115
+ </motion.span>
116
+ )}
117
+ {Icon && <Icon className="relative h-[1.1rem] w-[1.1rem] shrink-0" style={isActive ? { color: accentColor } : undefined} />}
118
+ {!collapsed && <span className="relative min-w-0 flex-1 truncate text-left">{item.label}</span>}
119
+ {!collapsed && item.badge != null && (
120
+ <span className="relative rounded-full px-1.5 py-px text-[0.68rem] font-semibold text-black" style={{ background: accentColor }}>
121
+ {item.badge}
122
+ </span>
123
+ )}
124
+ {collapsed && item.badge != null && <span className="absolute right-2 top-1.5 h-1.5 w-1.5 rounded-full" style={{ background: accentColor }} />}
125
+ {!collapsed && hasChildren && <ChevronDown className={`relative h-4 w-4 opacity-60 transition-transform ${isOpen ? "rotate-180" : ""}`} />}
126
+ </button>
127
+ <AnimatePresence initial={false}>
128
+ {hasChildren && isOpen && !collapsed && (
129
+ <motion.ul
130
+ initial={{ height: 0, opacity: 0 }}
131
+ animate={{ height: "auto", opacity: 1 }}
132
+ exit={{ height: 0, opacity: 0 }}
133
+ transition={{ duration: 0.22, ease: [0.22, 1, 0.36, 1] }}
134
+ className="relative mt-0.5 space-y-0.5 overflow-hidden"
135
+ >
136
+ <span aria-hidden="true" className={`absolute bottom-2 left-[1.3rem] top-1 w-px ${theme === "light" ? "bg-black/10" : "bg-white/10"}`} />
137
+ {item.children.map((child) => renderRow(child, depth + 1))}
138
+ </motion.ul>
139
+ )}
140
+ </AnimatePresence>
141
+ </li>
142
+ );
143
+ };
144
+
145
+ return (
146
+ <motion.aside
147
+ ref={asideRef}
148
+ aria-label="Sidebar"
149
+ animate={{ width: collapsed ? "4.25rem" : "15.5rem" }}
150
+ transition={{ type: "spring", stiffness: 400, damping: 40 }}
151
+ className={`relative flex h-full shrink-0 flex-col border-r ${t.panel} ${className}`}
152
+ >
153
+ <div className={`flex items-center gap-2.5 px-3.5 ${collapsed ? "flex-col justify-center gap-2 py-3" : "h-14"}`}>
154
+ <span className="grid h-8 w-8 shrink-0 place-items-center rounded-lg font-bold text-black" style={{ background: accentColor }}>
155
+ {brand.logo ? <img src={brand.logo} alt="" className="h-5 w-5" /> : brand.name?.[0]}
156
+ </span>
157
+ {!collapsed && <span className="truncate text-[0.95rem] font-semibold">{brand.name}</span>}
158
+ <button
159
+ type="button"
160
+ aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"}
161
+ onClick={() => {
162
+ setCollapsed((c) => !c);
163
+ setTip(null);
164
+ }}
165
+ className={`grid h-8 w-8 place-items-center rounded-lg ${t.item} ${collapsed ? "" : "ml-auto"}`}
166
+ >
167
+ {collapsed ? <PanelLeftOpen className="h-4 w-4" /> : <PanelLeftClose className="h-4 w-4" />}
168
+ </button>
169
+ </div>
170
+
171
+ <LayoutGroup id={pillId}>
172
+ <nav className="relative flex-1 overflow-y-auto overflow-x-hidden px-2.5 pb-3" style={{ scrollbarWidth: "thin", scrollbarColor: `${theme === "light" ? "rgba(0,0,0,.15)" : "rgba(255,255,255,.15)"} transparent` }}>
173
+ {sections.map((section) => (
174
+ <div key={section.title} className="mt-3 first:mt-1">
175
+ {!collapsed ? (
176
+ <p className={`mb-1.5 px-2.5 text-[0.68rem] font-semibold uppercase tracking-[0.14em] ${t.muted}`}>{section.title}</p>
177
+ ) : (
178
+ <span aria-hidden="true" className={`mx-auto mb-2 block h-px w-6 ${theme === "light" ? "bg-black/10" : "bg-white/10"}`} />
179
+ )}
180
+ <ul className="space-y-0.5">
181
+ {section.items.map((item) => renderRow(item))}
182
+ </ul>
183
+ </div>
184
+ ))}
185
+ </nav>
186
+ </LayoutGroup>
187
+
188
+ <AnimatePresence>
189
+ {collapsed && tip && (
190
+ <motion.span
191
+ key={tip.label}
192
+ role="tooltip"
193
+ initial={{ opacity: 0, x: -4 }}
194
+ animate={{ opacity: 1, x: 0 }}
195
+ exit={{ opacity: 0 }}
196
+ transition={{ duration: 0.12 }}
197
+ className={`pointer-events-none absolute left-[4.6rem] z-50 whitespace-nowrap rounded-md px-2 py-1 text-[0.75rem] font-medium shadow-lg ${t.tip}`}
198
+ style={{ top: tip.top, translateY: "-50%" }}
199
+ >
200
+ {tip.label}
201
+ </motion.span>
202
+ )}
203
+ </AnimatePresence>
204
+
205
+ {user && (
206
+ <div className={`flex items-center gap-3 border-t p-3 ${theme === "light" ? "border-black/10" : "border-white/10"} ${collapsed ? "justify-center" : ""}`}>
207
+ <img src={user.avatar} alt="" className="h-9 w-9 shrink-0 rounded-full object-cover ring-2" style={{ "--tw-ring-color": accentColor }} />
208
+ {!collapsed && (
209
+ <div className="min-w-0">
210
+ <p className="truncate text-[0.85rem] font-semibold">{user.name}</p>
211
+ <p className={`truncate text-[0.75rem] ${t.muted}`}>{user.email}</p>
212
+ </div>
213
+ )}
214
+ </div>
215
+ )}
216
+ </motion.aside>
217
+ );
218
+ };
219
+
220
+ export default SidebarNav;
@@ -0,0 +1,110 @@
1
+ import React, { useEffect, useRef, useState } from "react";
2
+ import { motion } from "framer-motion";
3
+ import { BarChart3, Globe2, Lock, Rocket, Sparkles, Zap } from "lucide-react";
4
+
5
+ /*
6
+ * Spotlight Grid: a bento grid of cards sharing one pointer spotlight. The
7
+ * light lands on every card's border and surface at once, so moving across
8
+ * the grid feels like sweeping a torch over it.
9
+ */
10
+
11
+ const DEFAULT_ITEMS = [
12
+ { title: "Lightning fast", description: "Ships as tiny chunks and loads only what a page needs.", icon: Zap, span: 2 },
13
+ { title: "Secure by default", description: "No tracking, no cookies, nothing to configure.", icon: Lock },
14
+ { title: "Global edge", description: "Served from 300+ cities close to your users.", icon: Globe2 },
15
+ { title: "Insights", description: "Real-time numbers for every component you add.", icon: BarChart3 },
16
+ { title: "Launch in minutes", description: "One CLI command copies the code into your project.", icon: Rocket },
17
+ { title: "Delightful motion", description: "Spring physics and GPU-friendly transforms everywhere.", icon: Sparkles, span: 2 },
18
+ ];
19
+
20
+ const THEMES = {
21
+ dark: { card: "bg-neutral-950 text-white", base: "rgba(255,255,255,0.08)", muted: "text-white/55", icon: "bg-white/[0.06] text-white", edge: (c) => c, glow: 12 },
22
+ // On white, a bright accent washes out: darken the border light and strengthen the glow.
23
+ light: { card: "bg-white text-neutral-900 shadow-[0_1px_2px_rgba(0,0,0,0.04)]", base: "rgba(0,0,0,0.09)", muted: "text-neutral-500", icon: "bg-black/[0.04] text-neutral-900", edge: (c) => `color-mix(in srgb, ${c} 70%, #000)`, glow: 24 },
24
+ };
25
+
26
+ // "auto" follows the visitor's light / dark system setting.
27
+ function useResolvedTheme(theme) {
28
+ const [system, setSystem] = useState("dark");
29
+ useEffect(() => {
30
+ if (theme !== "auto") return undefined;
31
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
32
+ const update = () => setSystem(mq.matches ? "dark" : "light");
33
+ update();
34
+ mq.addEventListener("change", update);
35
+ return () => mq.removeEventListener("change", update);
36
+ }, [theme]);
37
+ return theme === "auto" ? system : theme;
38
+ }
39
+
40
+ const SpotlightGrid = ({ items = DEFAULT_ITEMS, color = "#a3e635", size = 360, theme = "auto", minCardWidth = 12, className = "" }) => {
41
+ const ref = useRef(null);
42
+ const t = THEMES[useResolvedTheme(theme)] ?? THEMES.dark;
43
+
44
+ // Every card gets the pointer position in its own coordinates.
45
+ const onMove = (e) => {
46
+ ref.current.querySelectorAll("[data-spot]").forEach((card) => {
47
+ const r = card.getBoundingClientRect();
48
+ card.style.setProperty("--mx", `${e.clientX - r.left}px`);
49
+ card.style.setProperty("--my", `${e.clientY - r.top}px`);
50
+ card.style.setProperty("--spot", "1");
51
+ });
52
+ };
53
+ const onLeave = () => ref.current.querySelectorAll("[data-spot]").forEach((card) => card.style.setProperty("--spot", "0"));
54
+
55
+ return (
56
+ <div
57
+ ref={ref}
58
+ onPointerMove={onMove}
59
+ onPointerLeave={onLeave}
60
+ className={`grid w-full gap-3 ${className}`}
61
+ style={{ gridTemplateColumns: `repeat(auto-fit, minmax(min(100%, ${minCardWidth}rem), 1fr))` }}
62
+ >
63
+ {items.map((item, i) => {
64
+ const Icon = item.icon;
65
+ return (
66
+ <motion.article
67
+ key={item.title ?? i}
68
+ data-spot=""
69
+ initial={{ opacity: 0, y: 12 }}
70
+ whileInView={{ opacity: 1, y: 0 }}
71
+ viewport={{ once: true }}
72
+ transition={{ duration: 0.45, delay: i * 0.05 }}
73
+ className={`relative overflow-hidden rounded-2xl p-5 ${item.span > 1 ? "sm:col-span-2" : ""} ${t.card}`}
74
+ style={{ "--spot": 0, boxShadow: `inset 0 0 0 1px ${t.base}` }}
75
+ >
76
+ {/* Border light: a ring-shaped mask over a radial gradient. */}
77
+ <span
78
+ aria-hidden="true"
79
+ className="pointer-events-none absolute inset-0 rounded-[inherit] p-px transition-opacity duration-300"
80
+ style={{
81
+ opacity: "var(--spot)",
82
+ background: `radial-gradient(${size}px circle at var(--mx) var(--my), ${t.edge(color)}, transparent 45%)`,
83
+ WebkitMask: "linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)",
84
+ WebkitMaskComposite: "xor",
85
+ mask: "linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0)",
86
+ }}
87
+ />
88
+ {/* Surface glow. */}
89
+ <span
90
+ aria-hidden="true"
91
+ className="pointer-events-none absolute inset-0 transition-opacity duration-300"
92
+ style={{ opacity: "var(--spot)", background: `radial-gradient(${size * 1.2}px circle at var(--mx) var(--my), color-mix(in srgb, ${color} ${t.glow}%, transparent), transparent 45%)` }}
93
+ />
94
+ <div className="relative">
95
+ {Icon && (
96
+ <span className={`mb-4 grid h-10 w-10 place-items-center rounded-xl ${t.icon}`}>
97
+ <Icon className="h-5 w-5" />
98
+ </span>
99
+ )}
100
+ <h3 className="text-[1rem] font-semibold">{item.title}</h3>
101
+ {item.description && <p className={`mt-1.5 text-[0.85rem] leading-relaxed ${t.muted}`}>{item.description}</p>}
102
+ </div>
103
+ </motion.article>
104
+ );
105
+ })}
106
+ </div>
107
+ );
108
+ };
109
+
110
+ export default SpotlightGrid;
@@ -0,0 +1,108 @@
1
+ import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react";
2
+ import { AnimatePresence, motion } from "framer-motion";
3
+ import { AlertTriangle, CheckCircle2, Info, X, XCircle } from "lucide-react";
4
+
5
+ /*
6
+ * Toast Stack: notifications pile up as a neat stack and fan out on hover.
7
+ * Each one closes itself after `duration` (paused while hovered) or can be
8
+ * swiped away. Call ref.current.show({ title, description, type }) to add one.
9
+ */
10
+
11
+ const ICONS = {
12
+ success: { icon: CheckCircle2, color: "#22c55e" },
13
+ error: { icon: XCircle, color: "#ef4444" },
14
+ warning: { icon: AlertTriangle, color: "#f59e0b" },
15
+ info: { icon: Info, color: "#3b82f6" },
16
+ };
17
+
18
+ const ToastStack = forwardRef(function ToastStack({ position = "bottom-right", duration = 4000, max = 4, theme = "dark", contained = false, className = "" }, ref) {
19
+ const [toasts, setToasts] = useState([]);
20
+ const [hovered, setHovered] = useState(false);
21
+ const timers = useRef(new Map());
22
+ const dark = theme !== "light";
23
+ const top = position.startsWith("top");
24
+
25
+ const dismiss = useCallback((id) => {
26
+ clearTimeout(timers.current.get(id));
27
+ timers.current.delete(id);
28
+ setToasts((list) => list.filter((t) => t.id !== id));
29
+ }, []);
30
+
31
+ useImperativeHandle(ref, () => ({
32
+ show: (toast) => {
33
+ const id = `${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
34
+ setToasts((list) => [...list, { type: "info", ...toast, id }].slice(-max));
35
+ return id;
36
+ },
37
+ dismiss,
38
+ }));
39
+
40
+ // Auto-close, paused while the stack is hovered.
41
+ useEffect(() => {
42
+ if (hovered) {
43
+ timers.current.forEach((t) => clearTimeout(t));
44
+ timers.current.clear();
45
+ return undefined;
46
+ }
47
+ toasts.forEach((t) => {
48
+ if (!timers.current.has(t.id)) timers.current.set(t.id, setTimeout(() => dismiss(t.id), duration));
49
+ });
50
+ return undefined;
51
+ }, [toasts, hovered, duration, dismiss]);
52
+
53
+ const place = {
54
+ "bottom-right": "bottom-4 right-4 items-end",
55
+ "bottom-left": "bottom-4 left-4 items-start",
56
+ "top-right": "top-4 right-4 items-end",
57
+ "top-left": "top-4 left-4 items-start",
58
+ "bottom-center": "bottom-4 left-1/2 -translate-x-1/2 items-center",
59
+ "top-center": "top-4 left-1/2 -translate-x-1/2 items-center",
60
+ }[position];
61
+
62
+ return (
63
+ <ol
64
+ aria-live="polite"
65
+ onPointerEnter={() => setHovered(true)}
66
+ onPointerLeave={() => setHovered(false)}
67
+ className={`${contained ? "absolute" : "fixed"} z-[200] flex w-[min(22rem,calc(100%-2rem))] flex-col ${place} ${className}`}
68
+ >
69
+ <AnimatePresence initial={false}>
70
+ {toasts.map((t, i) => {
71
+ const fromEnd = toasts.length - 1 - i;
72
+ const { icon: Icon, color } = ICONS[t.type] ?? ICONS.info;
73
+ // Collapsed: cards peek out behind the newest. Hovered: they fan out into a list.
74
+ const offset = hovered ? fromEnd * 76 : fromEnd * 10;
75
+ return (
76
+ <motion.li
77
+ key={t.id}
78
+ layout
79
+ drag="x"
80
+ dragConstraints={{ left: 0, right: 0 }}
81
+ dragElastic={0.6}
82
+ onDragEnd={(_, info) => Math.abs(info.offset.x) > 90 && dismiss(t.id)}
83
+ initial={{ opacity: 0, y: top ? -30 : 30, scale: 0.94 }}
84
+ animate={{ opacity: fromEnd > 2 && !hovered ? 0 : 1, y: top ? offset : -offset, scale: hovered ? 1 : 1 - fromEnd * 0.05 }}
85
+ exit={{ opacity: 0, scale: 0.9, transition: { duration: 0.18 } }}
86
+ transition={{ type: "spring", stiffness: 380, damping: 32 }}
87
+ className={`absolute w-full cursor-grab touch-pan-y list-none active:cursor-grabbing ${top ? "top-0" : "bottom-0"}`}
88
+ style={{ zIndex: i }}
89
+ >
90
+ <div className={`flex items-start gap-3 rounded-2xl border p-3.5 pr-9 shadow-[0_18px_40px_-18px_rgba(0,0,0,0.5)] backdrop-blur ${dark ? "border-white/10 bg-neutral-900/95 text-white" : "border-black/10 bg-white/95 text-neutral-900"}`}>
91
+ <Icon className="mt-0.5 h-5 w-5 shrink-0" style={{ color }} />
92
+ <div className="min-w-0">
93
+ <p className="text-[0.88rem] font-semibold">{t.title}</p>
94
+ {t.description && <p className={`mt-0.5 text-[0.8rem] ${dark ? "text-white/60" : "text-neutral-500"}`}>{t.description}</p>}
95
+ </div>
96
+ <button type="button" aria-label="Dismiss" onClick={() => dismiss(t.id)} className={`absolute right-2.5 top-2.5 grid h-6 w-6 place-items-center rounded-full ${dark ? "text-white/50 hover:bg-white/10 hover:text-white" : "text-neutral-400 hover:bg-black/5 hover:text-neutral-900"}`}>
97
+ <X className="h-3.5 w-3.5" />
98
+ </button>
99
+ </div>
100
+ </motion.li>
101
+ );
102
+ })}
103
+ </AnimatePresence>
104
+ </ol>
105
+ );
106
+ });
107
+
108
+ export default ToastStack;
@@ -64,8 +64,7 @@ const ToolTip = ({ items }) => {
64
64
  <div className={`pointer-events-none absolute bottom-full mb-4 z-50 ${box}`}>
65
65
  {/* The card and its arrow animate together, so the arrow follows every swing. */}
66
66
  <div ref={(el) => (tooltipRefs.current[idx] = el)} className="relative origin-bottom">
67
- <div className="relative overflow-hidden rounded-xl border border-white/10 bg-zinc-950 px-3.5 py-2.5 shadow-[0_16px_40px_-12px_rgba(0,0,0,0.7)]">
68
- <span className="absolute inset-x-3 top-0 h-px bg-gradient-to-r from-transparent via-lime-400 to-cyan-400" />
67
+ <div className="relative overflow-hidden rounded-xl border border-white/10 bg-zinc-950/95 px-3.5 py-2.5 shadow-[inset_0_1px_0_rgba(255,255,255,0.08),0_16px_40px_-12px_rgba(0,0,0,0.7)] backdrop-blur">
69
68
  <span className="flex items-center gap-1.5 whitespace-nowrap text-sm font-semibold text-white">
70
69
  <span className="h-1.5 w-1.5 rounded-full bg-lime-400 shadow-[0_0_8px_#a3e635]" />
71
70
  {item.name}
@@ -80,7 +79,10 @@ const ToolTip = ({ items }) => {
80
79
  </div>
81
80
  </div>
82
81
  )}
83
- <div ref={(el) => (avatarRefs.current[idx] = el)} className="rounded-full bg-gradient-to-br from-lime-400 via-emerald-400 to-cyan-400 p-[2px] shadow-lg shadow-black/30">
82
+ <div
83
+ ref={(el) => (avatarRefs.current[idx] = el)}
84
+ className={`rounded-full p-[2px] shadow-lg shadow-black/30 transition-[background-color,box-shadow] duration-300 ${hoveredIndex === idx ? "bg-lime-400 shadow-[0_0_20px_-4px_#a3e635]" : "bg-zinc-700"}`}
85
+ >
84
86
  <img
85
87
  src={item.image}
86
88
  alt={item.name}