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,306 @@
1
+ import React, { useEffect, useRef, useState } from "react";
2
+ import { motion, useMotionValue, useReducedMotion, useSpring } from "framer-motion";
3
+
4
+ /*
5
+ * Magnetic Cursor: a dot + ring cursor for one area of the page. The ring
6
+ * wraps around links and buttons it hovers, grows into a labelled bubble over
7
+ * anything with data-cursor-text, and pulls data-magnetic elements toward the
8
+ * pointer. An optional tail follows it: comet, dots, sparkles or ghost rings.
9
+ * Touch devices keep their normal behaviour.
10
+ */
11
+
12
+ const TARGETS = "a, button, [role='button'], [data-cursor], [data-cursor-text], [data-magnetic]";
13
+
14
+ const hexToRgb = (hex) => {
15
+ let h = String(hex).replace("#", "");
16
+ if (h.length === 3) h = h.split("").map((c) => c + c).join("");
17
+ const n = parseInt(h, 16) || 0;
18
+ return `${(n >> 16) & 255},${(n >> 8) & 255},${n & 255}`;
19
+ };
20
+
21
+ /*
22
+ * The tail lives on one canvas. `feed` gets every pointer position; the loop
23
+ * only runs while something is still visible, then sleeps.
24
+ */
25
+ function useTrail(canvasRef, style, color, enabled) {
26
+ const state = useRef({ points: [], dots: [], parts: [], rings: [], last: null, raf: 0, lastSpawn: 0 });
27
+
28
+ useEffect(() => {
29
+ const canvas = canvasRef.current;
30
+ if (!canvas || !enabled || style === "none") return undefined;
31
+ const ctx = canvas.getContext("2d");
32
+ const s = state.current;
33
+ const rgb = hexToRgb(color);
34
+ const resize = () => {
35
+ const r = canvas.getBoundingClientRect();
36
+ const dpr = Math.min(window.devicePixelRatio || 1, 2);
37
+ canvas.width = Math.round(r.width * dpr);
38
+ canvas.height = Math.round(r.height * dpr);
39
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
40
+ };
41
+ const ro = new ResizeObserver(resize);
42
+ ro.observe(canvas);
43
+ resize();
44
+
45
+ const frame = () => {
46
+ const now = performance.now();
47
+ const { width, height } = canvas.getBoundingClientRect();
48
+ ctx.clearRect(0, 0, width, height);
49
+ let alive = false;
50
+
51
+ if (style === "comet") {
52
+ s.points = s.points.filter((p) => now - p.t < 320);
53
+ const n = s.points.length;
54
+ if (n > 1) {
55
+ alive = true;
56
+ ctx.lineCap = "round";
57
+ ctx.shadowColor = `rgba(${rgb},0.8)`;
58
+ ctx.shadowBlur = 12;
59
+ for (let i = 1; i < n; i++) {
60
+ const k = i / n;
61
+ ctx.strokeStyle = `rgba(${rgb},${k * 0.9})`;
62
+ ctx.lineWidth = 1 + k * 7;
63
+ ctx.beginPath();
64
+ ctx.moveTo(s.points[i - 1].x, s.points[i - 1].y);
65
+ ctx.lineTo(s.points[i].x, s.points[i].y);
66
+ ctx.stroke();
67
+ }
68
+ ctx.shadowBlur = 0;
69
+ }
70
+ }
71
+
72
+ if (style === "dots" && s.last) {
73
+ // Each dot eases toward the one ahead of it.
74
+ if (!s.dots.length) s.dots = Array.from({ length: 14 }, () => ({ x: s.last.x, y: s.last.y }));
75
+ let tx = s.last.x;
76
+ let ty = s.last.y;
77
+ s.dots.forEach((d, i) => {
78
+ d.x += (tx - d.x) * 0.38;
79
+ d.y += (ty - d.y) * 0.38;
80
+ tx = d.x;
81
+ ty = d.y;
82
+ const k = 1 - i / s.dots.length;
83
+ ctx.fillStyle = `rgba(${rgb},${k * 0.8})`;
84
+ ctx.beginPath();
85
+ ctx.arc(d.x, d.y, 1.5 + k * 4, 0, Math.PI * 2);
86
+ ctx.fill();
87
+ });
88
+ const tail = s.dots[s.dots.length - 1];
89
+ alive = Math.hypot(tail.x - s.last.x, tail.y - s.last.y) > 0.5;
90
+ }
91
+
92
+ if (style === "sparkles") {
93
+ s.parts = s.parts.filter((p) => now - p.born < p.life);
94
+ s.parts.forEach((p) => {
95
+ const k = 1 - (now - p.born) / p.life;
96
+ p.x += p.vx;
97
+ p.y += p.vy;
98
+ p.vy += 0.05;
99
+ p.vx *= 0.97;
100
+ const size = p.size * k;
101
+ ctx.strokeStyle = p.white ? `rgba(255,255,255,${k})` : `rgba(${rgb},${k})`;
102
+ ctx.lineWidth = 1.4;
103
+ ctx.beginPath();
104
+ ctx.moveTo(p.x - size, p.y);
105
+ ctx.lineTo(p.x + size, p.y);
106
+ ctx.moveTo(p.x, p.y - size);
107
+ ctx.lineTo(p.x, p.y + size);
108
+ ctx.stroke();
109
+ });
110
+ alive = s.parts.length > 0;
111
+ }
112
+
113
+ if (style === "ghost") {
114
+ s.rings = s.rings.filter((r) => now - r.born < 650);
115
+ s.rings.forEach((r) => {
116
+ const k = (now - r.born) / 650;
117
+ ctx.strokeStyle = `rgba(${rgb},${(1 - k) * 0.7})`;
118
+ ctx.lineWidth = 2 * (1 - k) + 0.5;
119
+ ctx.beginPath();
120
+ ctx.arc(r.x, r.y, 6 + k * 22, 0, Math.PI * 2);
121
+ ctx.stroke();
122
+ });
123
+ alive = s.rings.length > 0;
124
+ }
125
+
126
+ s.raf = alive ? requestAnimationFrame(frame) : 0;
127
+ };
128
+
129
+ s.wake = () => {
130
+ if (!s.raf) s.raf = requestAnimationFrame(frame);
131
+ };
132
+ return () => {
133
+ cancelAnimationFrame(s.raf);
134
+ s.raf = 0;
135
+ s.wake = null;
136
+ ro.disconnect();
137
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
138
+ s.points = [];
139
+ s.dots = [];
140
+ s.parts = [];
141
+ s.rings = [];
142
+ };
143
+ }, [canvasRef, style, color, enabled]);
144
+
145
+ // Called on every pointer move with coordinates inside the area.
146
+ return (x, y) => {
147
+ const s = state.current;
148
+ const now = performance.now();
149
+ const prev = s.last;
150
+ s.last = { x, y };
151
+ if (style === "comet") s.points.push({ x, y, t: now });
152
+ if (style === "sparkles" && prev) {
153
+ const speed = Math.hypot(x - prev.x, y - prev.y);
154
+ for (let i = 0; i < Math.min(4, Math.ceil(speed / 8)); i++) {
155
+ s.parts.push({ x, y, vx: (Math.random() - 0.5) * 2.4, vy: (Math.random() - 0.7) * 2.4, size: 2 + Math.random() * 3, life: 500 + Math.random() * 400, born: now, white: Math.random() < 0.3 });
156
+ }
157
+ }
158
+ if (style === "ghost" && now - s.lastSpawn > 70) {
159
+ s.lastSpawn = now;
160
+ s.rings.push({ x, y, born: now });
161
+ }
162
+ s.wake?.();
163
+ };
164
+ }
165
+
166
+ const MagneticCursor = ({
167
+ children,
168
+ color = "#a3e635",
169
+ ringSize = 2.25,
170
+ padding = 6,
171
+ magnetStrength = 0.35,
172
+ blend = false,
173
+ trail = "none",
174
+ className = "",
175
+ }) => {
176
+ const ref = useRef(null);
177
+ const trailRef = useRef(null);
178
+ const magnet = useRef(null);
179
+ const reduced = useReducedMotion();
180
+ const feedTrail = useTrail(trailRef, trail, color, !reduced);
181
+ const [visible, setVisible] = useState(false);
182
+ const [label, setLabel] = useState("");
183
+ const [pressed, setPressed] = useState(false);
184
+
185
+ const springy = reduced ? { duration: 0 } : { stiffness: 420, damping: 34, mass: 0.6 };
186
+ const dotX = useMotionValue(-100);
187
+ const dotY = useMotionValue(-100);
188
+ const x = useSpring(-100, springy);
189
+ const y = useSpring(-100, springy);
190
+ const w = useSpring(36, springy);
191
+ const h = useSpring(36, springy);
192
+ const radius = useSpring(18, springy);
193
+
194
+ const ringPx = () => ringSize * parseFloat(getComputedStyle(document.documentElement).fontSize || "16");
195
+
196
+ const releaseMagnet = () => {
197
+ if (magnet.current) {
198
+ magnet.current.style.transform = "";
199
+ magnet.current = null;
200
+ }
201
+ };
202
+
203
+ const onMove = (e) => {
204
+ if (e.pointerType !== "mouse") return;
205
+ const box = ref.current.getBoundingClientRect();
206
+ const px = e.clientX - box.left;
207
+ const py = e.clientY - box.top;
208
+ dotX.set(px);
209
+ dotY.set(py);
210
+ setVisible(true);
211
+ feedTrail(px, py);
212
+
213
+ const target = e.target.closest?.(TARGETS);
214
+ const inside = target && ref.current.contains(target) && target !== ref.current;
215
+ const text = inside ? target.getAttribute("data-cursor-text") : null;
216
+
217
+ if (inside && target.hasAttribute("data-magnetic")) {
218
+ const r = target.getBoundingClientRect();
219
+ const mx = (e.clientX - (r.left + r.width / 2)) * magnetStrength;
220
+ const my = (e.clientY - (r.top + r.height / 2)) * magnetStrength;
221
+ target.style.transition = "transform 0.2s ease-out";
222
+ target.style.transform = `translate(${mx}px, ${my}px)`;
223
+ if (magnet.current !== target) releaseMagnet();
224
+ magnet.current = target;
225
+ } else {
226
+ releaseMagnet();
227
+ }
228
+
229
+ if (text) {
230
+ // Big labelled bubble that follows the pointer.
231
+ const size = ringPx() * 2.2;
232
+ setLabel(text);
233
+ x.set(px - size / 2);
234
+ y.set(py - size / 2);
235
+ w.set(size);
236
+ h.set(size);
237
+ radius.set(size / 2);
238
+ } else if (inside) {
239
+ // Wrap the hovered element (it may be shifted by the magnet).
240
+ const r = target.getBoundingClientRect();
241
+ const br = parseFloat(getComputedStyle(target).borderTopLeftRadius) || 8;
242
+ setLabel("");
243
+ x.set(r.left - box.left - padding);
244
+ y.set(r.top - box.top - padding);
245
+ w.set(r.width + padding * 2);
246
+ h.set(r.height + padding * 2);
247
+ radius.set(Math.min(br + padding, (r.height + padding * 2) / 2));
248
+ } else {
249
+ const size = ringPx();
250
+ setLabel("");
251
+ x.set(px - size / 2);
252
+ y.set(py - size / 2);
253
+ w.set(size);
254
+ h.set(size);
255
+ radius.set(size / 2);
256
+ }
257
+ };
258
+
259
+ const onLeave = () => {
260
+ setVisible(false);
261
+ setLabel("");
262
+ releaseMagnet();
263
+ };
264
+
265
+ return (
266
+ <div
267
+ ref={ref}
268
+ onPointerMove={onMove}
269
+ onPointerLeave={onLeave}
270
+ onPointerDown={(e) => e.pointerType === "mouse" && setPressed(true)}
271
+ onPointerUp={() => setPressed(false)}
272
+ className={`relative ${visible ? "cursor-none [&_*]:cursor-none" : ""} ${className}`}
273
+ >
274
+ {children}
275
+ {trail !== "none" && <canvas ref={trailRef} aria-hidden="true" className="pointer-events-none absolute inset-0 z-40 h-full w-full" />}
276
+ <motion.div
277
+ aria-hidden="true"
278
+ className="pointer-events-none absolute left-0 top-0 z-50 flex items-center justify-center border-2 text-[0.72rem] font-semibold uppercase tracking-[0.14em]"
279
+ style={{
280
+ x,
281
+ y,
282
+ width: w,
283
+ height: h,
284
+ borderRadius: radius,
285
+ borderColor: color,
286
+ color: "#0a0a0a",
287
+ background: label ? color : `color-mix(in srgb, ${color} 12%, transparent)`,
288
+ mixBlendMode: blend ? "difference" : "normal",
289
+ }}
290
+ animate={{ opacity: visible ? 1 : 0, scale: pressed ? 0.88 : 1 }}
291
+ transition={{ duration: 0.18 }}
292
+ >
293
+ {label}
294
+ </motion.div>
295
+ <motion.div
296
+ aria-hidden="true"
297
+ className="pointer-events-none absolute -left-1 -top-1 z-50 h-2 w-2 rounded-full"
298
+ style={{ x: dotX, y: dotY, background: color }}
299
+ animate={{ opacity: visible && !label ? 1 : 0 }}
300
+ transition={{ duration: 0.12 }}
301
+ />
302
+ </div>
303
+ );
304
+ };
305
+
306
+ export default MagneticCursor;
@@ -0,0 +1,213 @@
1
+ import React, { useEffect, useId, useMemo, useState } from "react";
2
+ import { animate, motion, useMotionValue, useReducedMotion, useSpring, useTransform } from "framer-motion";
3
+ import { Check, Github, Linkedin, MapPin, Plus, Twitter } from "lucide-react";
4
+
5
+ /*
6
+ * Profile Card: one profile, two looks.
7
+ * card – cover, avatar, bio, stats, socials and a Follow button, with a soft 3D tilt
8
+ * lanyard – the same person as an ID badge hanging from a ribbon; drag it and
9
+ * let go to watch it swing back like a pendulum
10
+ */
11
+
12
+ const DEFAULT_PROFILE = {
13
+ name: "Mohan Dalei",
14
+ role: "Frontend Engineer",
15
+ company: "ApexUI",
16
+ location: "Bhubaneswar, India",
17
+ bio: "Building animated React components that feel alive. Coffee, motion and clean code.",
18
+ avatar: "https://ik.imagekit.io/ny6yinyut/apexui/assets/black-panther.png?tr=f-auto,q-80,w-320",
19
+ id: "APX-0042",
20
+ stats: [
21
+ { label: "Projects", value: "48" },
22
+ { label: "Followers", value: "12.8k" },
23
+ { label: "Stars", value: "3.2k" },
24
+ ],
25
+ socials: [
26
+ { icon: Github, href: "#", label: "GitHub" },
27
+ { icon: Twitter, href: "#", label: "Twitter" },
28
+ { icon: Linkedin, href: "#", label: "LinkedIn" },
29
+ ],
30
+ };
31
+
32
+ // Stable pseudo-random barcode bars from a string.
33
+ const barsFrom = (seed) => {
34
+ let h = 2166136261;
35
+ for (const ch of seed) h = Math.imul(h ^ ch.charCodeAt(0), 16777619);
36
+ return Array.from({ length: 34 }, (_, i) => 1 + (((h >>> (i % 24)) ^ (i * 7)) & 3));
37
+ };
38
+
39
+ function CardVariant({ p, color, theme }) {
40
+ const [following, setFollowing] = useState(false);
41
+ const reduced = useReducedMotion();
42
+ const rx = useSpring(0, { stiffness: 200, damping: 20 });
43
+ const ry = useSpring(0, { stiffness: 200, damping: 20 });
44
+ const dark = theme !== "light";
45
+
46
+ const onMove = (e) => {
47
+ if (reduced || e.pointerType !== "mouse") return;
48
+ const r = e.currentTarget.getBoundingClientRect();
49
+ ry.set(((e.clientX - r.left) / r.width - 0.5) * 10);
50
+ rx.set(-((e.clientY - r.top) / r.height - 0.5) * 10);
51
+ };
52
+
53
+ return (
54
+ <div style={{ perspective: 1000 }}>
55
+ <motion.article
56
+ onPointerMove={onMove}
57
+ onPointerLeave={() => {
58
+ rx.set(0);
59
+ ry.set(0);
60
+ }}
61
+ style={{ rotateX: rx, rotateY: ry }}
62
+ className={`w-[min(21rem,88vw)] overflow-hidden rounded-3xl border shadow-[0_30px_60px_-30px_rgba(0,0,0,0.5)] ${dark ? "border-white/10 bg-neutral-950 text-white" : "border-black/10 bg-white text-neutral-900"}`}
63
+ >
64
+ <div className="relative h-24" style={{ background: `radial-gradient(120% 140% at 0% 0%, ${color}, transparent 60%), radial-gradient(120% 140% at 100% 0%, #6366f1, transparent 60%), ${dark ? "#111" : "#eee"}` }}>
65
+ <div className="absolute inset-0 opacity-30 [background-image:radial-gradient(rgba(255,255,255,.6)_1px,transparent_1px)] [background-size:12px_12px]" />
66
+ </div>
67
+ <div className="px-6 pb-6">
68
+ <div className="-mt-11 flex items-end justify-between">
69
+ <div className="relative">
70
+ <img src={p.avatar} alt="" className={`h-[5.5rem] w-[5.5rem] rounded-full object-cover ring-4 ${dark ? "bg-neutral-800 ring-neutral-950" : "bg-neutral-200 ring-white"}`} />
71
+ <span className={`absolute bottom-1.5 right-1.5 h-3.5 w-3.5 rounded-full bg-emerald-400 ring-[3px] ${dark ? "ring-neutral-950" : "ring-white"}`} aria-label="Online" />
72
+ </div>
73
+ <motion.button
74
+ type="button"
75
+ onClick={() => setFollowing((f) => !f)}
76
+ whileTap={{ scale: 0.94 }}
77
+ layout
78
+ className="mb-1 inline-flex h-9 items-center gap-1.5 rounded-full px-4 text-[0.8rem] font-semibold"
79
+ style={following ? { background: "transparent", color: dark ? "#fff" : "#111", boxShadow: `inset 0 0 0 1.5px ${dark ? "rgba(255,255,255,.25)" : "rgba(0,0,0,.2)"}` } : { background: color, color: "#0a0a0a" }}
80
+ >
81
+ {following ? <Check className="h-3.5 w-3.5" /> : <Plus className="h-3.5 w-3.5" />}
82
+ {following ? "Following" : "Follow"}
83
+ </motion.button>
84
+ </div>
85
+ <h3 className="mt-3 text-lg font-semibold leading-tight">{p.name}</h3>
86
+ <p className={`text-[0.85rem] ${dark ? "text-white/55" : "text-neutral-500"}`}>
87
+ {p.role} · {p.company}
88
+ </p>
89
+ {p.location && (
90
+ <p className={`mt-1 flex items-center gap-1 text-[0.78rem] ${dark ? "text-white/45" : "text-neutral-400"}`}>
91
+ <MapPin className="h-3.5 w-3.5" /> {p.location}
92
+ </p>
93
+ )}
94
+ {p.bio && <p className={`mt-3 text-[0.85rem] leading-relaxed ${dark ? "text-white/75" : "text-neutral-600"}`}>{p.bio}</p>}
95
+ <dl className={`mt-4 grid grid-cols-3 divide-x rounded-2xl py-3 text-center ${dark ? "divide-white/10 bg-white/[0.04]" : "divide-black/10 bg-black/[0.03]"}`}>
96
+ {p.stats?.map((s) => (
97
+ <div key={s.label}>
98
+ <dd className="text-[1rem] font-semibold">{s.value}</dd>
99
+ <dt className={`text-[0.7rem] ${dark ? "text-white/45" : "text-neutral-500"}`}>{s.label}</dt>
100
+ </div>
101
+ ))}
102
+ </dl>
103
+ <div className="mt-4 flex gap-2">
104
+ {p.socials?.map(({ icon: Icon, href, label }) => (
105
+ <a key={label} href={href} aria-label={label} className={`grid h-9 w-9 place-items-center rounded-full transition hover:-translate-y-0.5 ${dark ? "bg-white/[0.06] hover:bg-white/[0.12]" : "bg-black/[0.04] hover:bg-black/[0.08]"}`}>
106
+ <Icon className="h-4 w-4" />
107
+ </a>
108
+ ))}
109
+ </div>
110
+ </div>
111
+ </motion.article>
112
+ </div>
113
+ );
114
+ }
115
+
116
+ function LanyardVariant({ p, color, theme, strapLength }) {
117
+ const pathId = `strap-${useId().replace(/[^a-zA-Z0-9-]/g, "")}`;
118
+ const reduced = useReducedMotion();
119
+ const x = useMotionValue(0);
120
+ const y = useMotionValue(reduced ? 0 : -140);
121
+ const length = strapLength * 16;
122
+ const bars = useMemo(() => barsFrom(p.id || p.name), [p.id, p.name]);
123
+ const dark = theme !== "light";
124
+ const swing = { type: "spring", stiffness: 90, damping: 5, mass: 1 };
125
+
126
+ // Drop in on mount.
127
+ useEffect(() => {
128
+ if (reduced) return undefined;
129
+ const a = animate(y, 0, { type: "spring", stiffness: 120, damping: 9 });
130
+ return () => a.stop();
131
+ }, [reduced, y]);
132
+
133
+ // The badge hangs along the strap, so its angle follows its offset.
134
+ const rotate = useTransform([x, y], ([vx, vy]) => (-Math.atan2(vx, Math.max(40, length + vy)) * 180) / Math.PI);
135
+ const d = useTransform([x, y], ([vx, vy]) => {
136
+ const ex = 160 + vx;
137
+ const ey = length + vy;
138
+ return `M160 0 Q ${160 + vx * 0.35} ${ey * 0.55} ${ex} ${ey}`;
139
+ });
140
+
141
+ return (
142
+ <div className="relative flex w-[20rem] max-w-full justify-center" style={{ height: `${strapLength + 22}rem` }}>
143
+ <svg className="pointer-events-none absolute left-1/2 top-0 -translate-x-1/2 overflow-visible" width="320" height={length + 40} aria-hidden="true">
144
+ <defs>
145
+ <motion.path id={pathId} d={d} />
146
+ </defs>
147
+ <motion.path d={d} fill="none" stroke={color} strokeWidth="26" strokeLinecap="butt" />
148
+ <motion.path d={d} fill="none" stroke="rgba(0,0,0,0.18)" strokeWidth="26" strokeDasharray="2 10" />
149
+ <text fontSize="11" fontWeight="800" letterSpacing="3" fill="#0a0a0a" dy="4">
150
+ <textPath href={`#${pathId}`} startOffset="8">
151
+ {`${(p.company || "APEXUI").toUpperCase()} • `.repeat(6)}
152
+ </textPath>
153
+ </text>
154
+ <rect x="148" y="-6" width="24" height="14" rx="3" fill={dark ? "#2a2a2a" : "#bbb"} />
155
+ </svg>
156
+
157
+ <motion.div
158
+ drag
159
+ dragMomentum={false}
160
+ onDragEnd={() => {
161
+ animate(x, 0, swing);
162
+ animate(y, 0, swing);
163
+ }}
164
+ whileDrag={{ cursor: "grabbing" }}
165
+ className="absolute left-1/2 cursor-grab touch-none"
166
+ style={{ x, y, rotate, top: length, marginLeft: "-7.25rem", transformOrigin: "50% 0%" }}
167
+ >
168
+ {/* Metal clip */}
169
+ <div className="mx-auto h-5 w-9 rounded-b-md rounded-t-sm bg-gradient-to-b from-neutral-300 to-neutral-500 shadow-inner" />
170
+ <article className={`-mt-1 w-[14.5rem] overflow-hidden rounded-2xl border shadow-[0_30px_60px_-24px_rgba(0,0,0,0.6)] ${dark ? "border-white/10 bg-neutral-950 text-white" : "border-black/10 bg-white text-neutral-900"}`}>
171
+ <div className="relative h-16 px-4 pt-3" style={{ background: color }}>
172
+ <span className="mx-auto block h-2.5 w-12 rounded-full bg-black/25" aria-hidden="true" />
173
+ <p className="mt-2 flex items-center justify-between text-[0.7rem] font-black uppercase tracking-[0.2em] text-black/80">
174
+ <span>{p.company}</span>
175
+ <span>Staff</span>
176
+ </p>
177
+ </div>
178
+ <div className="px-5 pb-5">
179
+ <img src={p.avatar} alt="" draggable="false" className={`relative -mt-8 h-24 w-24 rounded-2xl object-cover ring-4 ${dark ? "bg-neutral-800 ring-neutral-950" : "bg-neutral-200 ring-white"}`} />
180
+ <h3 className="mt-3 text-[1.05rem] font-bold leading-tight">{p.name}</h3>
181
+ <p className={`text-[0.78rem] ${dark ? "text-white/55" : "text-neutral-500"}`}>{p.role}</p>
182
+ <div className={`mt-3 grid grid-cols-2 gap-2 text-[0.68rem] ${dark ? "text-white/45" : "text-neutral-500"}`}>
183
+ <div>
184
+ <p className="uppercase tracking-wider">ID</p>
185
+ <p className={`font-mono text-[0.78rem] font-semibold ${dark ? "text-white" : "text-neutral-900"}`}>{p.id}</p>
186
+ </div>
187
+ <div>
188
+ <p className="uppercase tracking-wider">Location</p>
189
+ <p className={`truncate text-[0.78rem] font-semibold ${dark ? "text-white" : "text-neutral-900"}`}>{p.location?.split(",")[0]}</p>
190
+ </div>
191
+ </div>
192
+ <div className="mt-4 flex h-9 items-stretch gap-[2px]" aria-hidden="true">
193
+ {bars.map((w, i) => (
194
+ <span key={i} className={dark ? "bg-white/80" : "bg-neutral-900"} style={{ width: w, opacity: i % 5 === 0 ? 0.5 : 1 }} />
195
+ ))}
196
+ </div>
197
+ </div>
198
+ </article>
199
+ </motion.div>
200
+ </div>
201
+ );
202
+ }
203
+
204
+ const ProfileCard = ({ variant = "card", profile = DEFAULT_PROFILE, color = "#a3e635", theme = "dark", strapLength = 10, className = "" }) => {
205
+ const p = { ...DEFAULT_PROFILE, ...profile };
206
+ return (
207
+ <div className={`flex justify-center ${className}`}>
208
+ {variant === "lanyard" ? <LanyardVariant p={p} color={color} theme={theme} strapLength={strapLength} /> : <CardVariant p={p} color={color} theme={theme} />}
209
+ </div>
210
+ );
211
+ };
212
+
213
+ export default ProfileCard;