apex-ui-kit 2.1.3 → 2.3.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.
Files changed (64) hide show
  1. package/dist/ApexUI-Kit/templates/AiChat/AiChat.jsx +171 -0
  2. package/dist/ApexUI-Kit/templates/AiModelPrompt/AiModelPrompt.jsx +221 -0
  3. package/dist/ApexUI-Kit/templates/AiModelPrompt/index.js +2 -0
  4. package/dist/ApexUI-Kit/templates/AiModelPrompt/models.js +15 -0
  5. package/dist/ApexUI-Kit/templates/AiPromptBox/AiPromptBox.jsx +173 -0
  6. package/dist/ApexUI-Kit/templates/AiResponse/AiResponse.jsx +172 -0
  7. package/dist/ApexUI-Kit/templates/AiThinking/AiThinking.jsx +106 -0
  8. package/dist/ApexUI-Kit/templates/AiVoiceBars/AiVoiceBars.jsx +78 -0
  9. package/dist/ApexUI-Kit/templates/AiVoiceBlob/AiVoiceBlob.jsx +105 -0
  10. package/dist/ApexUI-Kit/templates/AiVoiceOrb/AiVoiceOrb.jsx +74 -0
  11. package/dist/ApexUI-Kit/templates/AiVoiceRing/AiVoiceRing.jsx +120 -0
  12. package/dist/ApexUI-Kit/templates/AiVoiceWave/AiVoiceWave.jsx +107 -0
  13. package/dist/ApexUI-Kit/templates/AnimatedButton/AnimatedButton.jsx +251 -0
  14. package/dist/ApexUI-Kit/templates/AnimatedSwitch/AnimatedSwitch.jsx +80 -0
  15. package/dist/ApexUI-Kit/templates/AnimatedTabs/AnimatedTabs.jsx +107 -0
  16. package/dist/ApexUI-Kit/templates/AuroraBackground/AuroraBackground.jsx +48 -0
  17. package/dist/ApexUI-Kit/templates/BlurRevealText/BlurRevealText.jsx +60 -0
  18. package/dist/ApexUI-Kit/templates/BounceCards/BounceCards.jsx +53 -0
  19. package/dist/ApexUI-Kit/templates/CardSwap3D/CardSwap3D.jsx +80 -0
  20. package/dist/ApexUI-Kit/templates/Carousel3D/Carousel3D.jsx +222 -0
  21. package/dist/ApexUI-Kit/templates/CircularText/CircularText.jsx +49 -0
  22. package/dist/ApexUI-Kit/templates/ClickSpark/ClickSpark.jsx +72 -0
  23. package/dist/ApexUI-Kit/templates/Coverflow3D/Coverflow3D.jsx +108 -0
  24. package/dist/ApexUI-Kit/templates/CubeGallery/CubeGallery.jsx +166 -0
  25. package/dist/ApexUI-Kit/templates/CursorTrail/CursorTrail.jsx +200 -0
  26. package/dist/ApexUI-Kit/templates/DotGlobe/DotGlobe.jsx +289 -0
  27. package/dist/ApexUI-Kit/templates/ElasticSlider/ElasticSlider.jsx +152 -0
  28. package/dist/ApexUI-Kit/templates/ExtrudedText/ExtrudedText.jsx +78 -0
  29. package/dist/ApexUI-Kit/templates/FaqAccordion/FaqAccordion.jsx +88 -0
  30. package/dist/ApexUI-Kit/templates/FlipBook/FlipBook.jsx +191 -0
  31. package/dist/ApexUI-Kit/templates/FlipClock/FlipClock.jsx +121 -0
  32. package/dist/ApexUI-Kit/templates/FloatingNavbar/FloatingNavbar.jsx +162 -0
  33. package/dist/ApexUI-Kit/templates/Folder/Folder.jsx +101 -0
  34. package/dist/ApexUI-Kit/templates/GlitchText/GlitchText.jsx +56 -0
  35. package/dist/ApexUI-Kit/templates/GooeyNav/GooeyNav.jsx +310 -0
  36. package/dist/ApexUI-Kit/templates/HoldToConfirm/HoldToConfirm.jsx +169 -0
  37. package/dist/ApexUI-Kit/templates/HoldToConfirm/UndoNotice.jsx +58 -0
  38. package/dist/ApexUI-Kit/templates/HoloCard/HoloCard.jsx +92 -0
  39. package/dist/ApexUI-Kit/templates/ImageCompare/ImageCompare.jsx +94 -0
  40. package/dist/ApexUI-Kit/templates/IsoStack/IsoStack.jsx +86 -0
  41. package/dist/ApexUI-Kit/templates/LiquidGradient/LiquidGradient.jsx +238 -0
  42. package/dist/ApexUI-Kit/templates/LoadingScreen/LoadingScreen.jsx +805 -0
  43. package/dist/ApexUI-Kit/templates/MagnetLines/MagnetLines.jsx +67 -0
  44. package/dist/ApexUI-Kit/templates/MagneticCursor/MagneticCursor.jsx +148 -0
  45. package/dist/ApexUI-Kit/templates/PixelTransition/PixelTransition.jsx +75 -0
  46. package/dist/ApexUI-Kit/templates/PricingCards/PricingCards.jsx +115 -0
  47. package/dist/ApexUI-Kit/templates/ProfileCard/ProfileCard.jsx +213 -0
  48. package/dist/ApexUI-Kit/templates/ProximityText/ProximityText.jsx +66 -0
  49. package/dist/ApexUI-Kit/templates/RotatingText/RotatingText.jsx +50 -0
  50. package/dist/ApexUI-Kit/templates/ShinyText/ShinyText.jsx +30 -0
  51. package/dist/ApexUI-Kit/templates/SidebarNav/SidebarNav.jsx +220 -0
  52. package/dist/ApexUI-Kit/templates/SpotlightGrid/SpotlightGrid.jsx +110 -0
  53. package/dist/ApexUI-Kit/templates/StarBorder/StarBorder.jsx +33 -0
  54. package/dist/ApexUI-Kit/templates/Stepper/Stepper.jsx +129 -0
  55. package/dist/ApexUI-Kit/templates/SwipeCards/SwipeCards.jsx +123 -0
  56. package/dist/ApexUI-Kit/templates/TextRing3D/TextRing3D.jsx +73 -0
  57. package/dist/ApexUI-Kit/templates/ToastStack/ToastStack.jsx +108 -0
  58. package/dist/ApexUI-Kit/templates/ToolTip/ToolTip.jsx +5 -3
  59. package/dist/ApexUI-Kit/templates/TrueFocus/TrueFocus.jsx +64 -0
  60. package/dist/ApexUI-Kit/templates/TypewriterText/TypewriterText.jsx +70 -0
  61. package/dist/ApexUI-Kit/templates/WebGLCarousel/WebGLCarousel.jsx +326 -0
  62. package/dist/ApexUI-Kit/templates/WorldMap/WorldMap.jsx +359 -0
  63. package/dist/cli/index.js +63 -0
  64. package/package.json +1 -1
@@ -0,0 +1,67 @@
1
+ import React, { useEffect, useRef } from "react";
2
+
3
+ /*
4
+ * Magnet Lines: a grid of short lines that all swing to point at the
5
+ * pointer, like iron filings around a magnet. Lines near the pointer glow
6
+ * and stretch a little. When the pointer leaves they drift back to a slow
7
+ * wave.
8
+ */
9
+
10
+ const MagnetLines = ({ rows = 9, columns = 9, size = 22, lineColor = "currentColor", activeColor = "#a3e635", lineWidth = 0.22, lineHeight = 1.6, baseAngle = -10, className = "" }) => {
11
+ const wrap = useRef(null);
12
+ const lines = useRef([]);
13
+ const pointer = useRef(null);
14
+
15
+ useEffect(() => {
16
+ let raf = 0;
17
+ const angles = lines.current.map(() => baseAngle);
18
+ const tick = (now) => {
19
+ const p = pointer.current;
20
+ const t = now / 1000;
21
+ lines.current.forEach((el, i) => {
22
+ if (!el) return;
23
+ const r = el.getBoundingClientRect();
24
+ const cx = r.left + r.width / 2;
25
+ const cy = r.top + r.height / 2;
26
+ let target;
27
+ let near = 0;
28
+ if (p) {
29
+ const dx = p.x - cx;
30
+ const dy = p.y - cy;
31
+ target = (Math.atan2(dy, dx) * 180) / Math.PI + 90;
32
+ near = Math.max(0, 1 - Math.hypot(dx, dy) / 220);
33
+ } else {
34
+ const col = i % columns;
35
+ const row = Math.floor(i / columns);
36
+ target = baseAngle + Math.sin(t * 1.2 + col * 0.5 + row * 0.3) * 25;
37
+ }
38
+ // Turn the short way round.
39
+ const diff = ((target - angles[i] + 540) % 360) - 180;
40
+ angles[i] += diff * 0.18;
41
+ el.style.transform = `rotate(${angles[i]}deg) scaleY(${1 + near * 0.35})`;
42
+ el.style.background = near > 0.05 ? `color-mix(in srgb, ${activeColor} ${Math.round(near * 100)}%, ${lineColor})` : lineColor;
43
+ el.style.opacity = String(0.35 + 0.65 * Math.max(near, p ? 0.25 : 0.4));
44
+ });
45
+ raf = requestAnimationFrame(tick);
46
+ };
47
+ raf = requestAnimationFrame(tick);
48
+ return () => cancelAnimationFrame(raf);
49
+ }, [baseAngle, columns, lineColor, activeColor]);
50
+
51
+ return (
52
+ <div
53
+ ref={wrap}
54
+ className={`grid touch-none place-items-center ${className}`}
55
+ style={{ gridTemplateColumns: `repeat(${columns}, 1fr)`, width: `min(${size}rem, 90vw)`, aspectRatio: `${columns} / ${rows}` }}
56
+ onPointerMove={(e) => (pointer.current = { x: e.clientX, y: e.clientY })}
57
+ onPointerLeave={() => (pointer.current = null)}
58
+ aria-hidden="true"
59
+ >
60
+ {Array.from({ length: rows * columns }, (_, i) => (
61
+ <span key={i} ref={(el) => (lines.current[i] = el)} className="block rounded-full will-change-transform" style={{ width: `${lineWidth}rem`, height: `${lineHeight}rem`, background: lineColor }} />
62
+ ))}
63
+ </div>
64
+ );
65
+ };
66
+
67
+ export default MagnetLines;
@@ -0,0 +1,148 @@
1
+ import React, { 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. Touch devices keep their normal behaviour.
9
+ */
10
+
11
+ const TARGETS = "a, button, [role='button'], [data-cursor], [data-cursor-text], [data-magnetic]";
12
+
13
+ const MagneticCursor = ({
14
+ children,
15
+ color = "#a3e635",
16
+ ringSize = 2.25,
17
+ padding = 6,
18
+ magnetStrength = 0.35,
19
+ blend = false,
20
+ className = "",
21
+ }) => {
22
+ const ref = useRef(null);
23
+ const magnet = useRef(null);
24
+ const reduced = useReducedMotion();
25
+ const [visible, setVisible] = useState(false);
26
+ const [label, setLabel] = useState("");
27
+ const [pressed, setPressed] = useState(false);
28
+
29
+ const springy = reduced ? { duration: 0 } : { stiffness: 420, damping: 34, mass: 0.6 };
30
+ const dotX = useMotionValue(-100);
31
+ const dotY = useMotionValue(-100);
32
+ const x = useSpring(-100, springy);
33
+ const y = useSpring(-100, springy);
34
+ const w = useSpring(36, springy);
35
+ const h = useSpring(36, springy);
36
+ const radius = useSpring(18, springy);
37
+
38
+ const ringPx = () => ringSize * parseFloat(getComputedStyle(document.documentElement).fontSize || "16");
39
+
40
+ const releaseMagnet = () => {
41
+ if (magnet.current) {
42
+ magnet.current.style.transform = "";
43
+ magnet.current = null;
44
+ }
45
+ };
46
+
47
+ const onMove = (e) => {
48
+ if (e.pointerType !== "mouse") return;
49
+ const box = ref.current.getBoundingClientRect();
50
+ const px = e.clientX - box.left;
51
+ const py = e.clientY - box.top;
52
+ dotX.set(px);
53
+ dotY.set(py);
54
+ setVisible(true);
55
+
56
+ const target = e.target.closest?.(TARGETS);
57
+ const inside = target && ref.current.contains(target) && target !== ref.current;
58
+ const text = inside ? target.getAttribute("data-cursor-text") : null;
59
+
60
+ if (inside && target.hasAttribute("data-magnetic")) {
61
+ const r = target.getBoundingClientRect();
62
+ const mx = (e.clientX - (r.left + r.width / 2)) * magnetStrength;
63
+ const my = (e.clientY - (r.top + r.height / 2)) * magnetStrength;
64
+ target.style.transition = "transform 0.2s ease-out";
65
+ target.style.transform = `translate(${mx}px, ${my}px)`;
66
+ if (magnet.current !== target) releaseMagnet();
67
+ magnet.current = target;
68
+ } else {
69
+ releaseMagnet();
70
+ }
71
+
72
+ if (text) {
73
+ // Big labelled bubble that follows the pointer.
74
+ const size = ringPx() * 2.2;
75
+ setLabel(text);
76
+ x.set(px - size / 2);
77
+ y.set(py - size / 2);
78
+ w.set(size);
79
+ h.set(size);
80
+ radius.set(size / 2);
81
+ } else if (inside) {
82
+ // Wrap the hovered element (it may be shifted by the magnet).
83
+ const r = target.getBoundingClientRect();
84
+ const br = parseFloat(getComputedStyle(target).borderTopLeftRadius) || 8;
85
+ setLabel("");
86
+ x.set(r.left - box.left - padding);
87
+ y.set(r.top - box.top - padding);
88
+ w.set(r.width + padding * 2);
89
+ h.set(r.height + padding * 2);
90
+ radius.set(Math.min(br + padding, (r.height + padding * 2) / 2));
91
+ } else {
92
+ const size = ringPx();
93
+ setLabel("");
94
+ x.set(px - size / 2);
95
+ y.set(py - size / 2);
96
+ w.set(size);
97
+ h.set(size);
98
+ radius.set(size / 2);
99
+ }
100
+ };
101
+
102
+ const onLeave = () => {
103
+ setVisible(false);
104
+ setLabel("");
105
+ releaseMagnet();
106
+ };
107
+
108
+ return (
109
+ <div
110
+ ref={ref}
111
+ onPointerMove={onMove}
112
+ onPointerLeave={onLeave}
113
+ onPointerDown={(e) => e.pointerType === "mouse" && setPressed(true)}
114
+ onPointerUp={() => setPressed(false)}
115
+ className={`relative ${visible ? "cursor-none [&_*]:cursor-none" : ""} ${className}`}
116
+ >
117
+ {children}
118
+ <motion.div
119
+ aria-hidden="true"
120
+ 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]"
121
+ style={{
122
+ x,
123
+ y,
124
+ width: w,
125
+ height: h,
126
+ borderRadius: radius,
127
+ borderColor: color,
128
+ color: "#0a0a0a",
129
+ background: label ? color : `color-mix(in srgb, ${color} 12%, transparent)`,
130
+ mixBlendMode: blend ? "difference" : "normal",
131
+ }}
132
+ animate={{ opacity: visible ? 1 : 0, scale: pressed ? 0.88 : 1 }}
133
+ transition={{ duration: 0.18 }}
134
+ >
135
+ {label}
136
+ </motion.div>
137
+ <motion.div
138
+ aria-hidden="true"
139
+ className="pointer-events-none absolute -left-1 -top-1 z-50 h-2 w-2 rounded-full"
140
+ style={{ x: dotX, y: dotY, background: color }}
141
+ animate={{ opacity: visible && !label ? 1 : 0 }}
142
+ transition={{ duration: 0.12 }}
143
+ />
144
+ </div>
145
+ );
146
+ };
147
+
148
+ export default MagneticCursor;
@@ -0,0 +1,75 @@
1
+ import React, { useEffect, useMemo, useRef, useState } from "react";
2
+
3
+ /*
4
+ * Pixel Transition: hover (or tap) the card and it dissolves into a grid of
5
+ * pixels that pop in at random, the content swaps underneath, then the
6
+ * pixels clear away to reveal the second side.
7
+ */
8
+
9
+ const IK = "https://ik.imagekit.io/ny6yinyut/apexui/assets";
10
+
11
+ const DefaultFirst = () => <img src={`${IK}/black-panther.png?tr=f-auto,q-80,w-600`} alt="Black Panther" draggable="false" className="h-full w-full bg-neutral-900 object-cover" />;
12
+ const DefaultSecond = () => (
13
+ <div className="grid h-full w-full place-items-center bg-neutral-950 p-6 text-center text-white">
14
+ <div>
15
+ <p className="text-[0.7rem] font-semibold uppercase tracking-[0.3em] text-white/50">Wakanda</p>
16
+ <p className="mt-2 text-3xl font-black">Black Panther</p>
17
+ <p className="mt-2 text-sm text-white/60">King T&apos;Challa — protector of his people.</p>
18
+ </div>
19
+ </div>
20
+ );
21
+
22
+ const shuffle = (arr) => {
23
+ const a = [...arr];
24
+ for (let i = a.length - 1; i > 0; i--) {
25
+ const j = Math.floor(Math.random() * (i + 1));
26
+ [a[i], a[j]] = [a[j], a[i]];
27
+ }
28
+ return a;
29
+ };
30
+
31
+ const PixelTransition = ({ first = <DefaultFirst />, second = <DefaultSecond />, gridSize = 10, pixelColor = "#a3e635", duration = 0.45, aspectRatio = "4 / 5", width = 16, className = "" }) => {
32
+ const [showSecond, setShowSecond] = useState(false);
33
+ const pixels = useRef([]);
34
+ const timers = useRef([]);
35
+ const target = useRef(false);
36
+ const cells = useMemo(() => Array.from({ length: gridSize * gridSize }, (_, i) => i), [gridSize]);
37
+
38
+ useEffect(() => () => timers.current.forEach(clearTimeout), []);
39
+
40
+ const run = (toSecond) => {
41
+ if (target.current === toSecond) return;
42
+ target.current = toSecond;
43
+ timers.current.forEach(clearTimeout);
44
+ timers.current = [];
45
+ const half = (duration * 1000) / 2;
46
+ const order = shuffle(cells);
47
+ const per = half / order.length;
48
+ order.forEach((c, k) => timers.current.push(setTimeout(() => pixels.current[c] && (pixels.current[c].style.opacity = "1"), k * per)));
49
+ timers.current.push(
50
+ setTimeout(() => {
51
+ setShowSecond(toSecond);
52
+ shuffle(cells).forEach((c, k) => timers.current.push(setTimeout(() => pixels.current[c] && (pixels.current[c].style.opacity = "0"), k * per)));
53
+ }, half + 30),
54
+ );
55
+ };
56
+
57
+ return (
58
+ <div
59
+ className={`relative cursor-pointer overflow-hidden rounded-2xl ring-1 ring-white/10 ${className}`}
60
+ style={{ aspectRatio, width: `min(${width}rem, 80vw)` }}
61
+ onPointerEnter={(e) => e.pointerType === "mouse" && run(true)}
62
+ onPointerLeave={(e) => e.pointerType === "mouse" && run(false)}
63
+ onClick={() => run(!target.current)}
64
+ >
65
+ <div className="absolute inset-0">{showSecond ? second : first}</div>
66
+ <div className="pointer-events-none absolute inset-0 grid" style={{ gridTemplateColumns: `repeat(${gridSize}, 1fr)`, gridTemplateRows: `repeat(${gridSize}, 1fr)` }} aria-hidden="true">
67
+ {cells.map((c) => (
68
+ <span key={c} ref={(el) => (pixels.current[c] = el)} style={{ background: pixelColor, opacity: 0 }} />
69
+ ))}
70
+ </div>
71
+ </div>
72
+ );
73
+ };
74
+
75
+ export default PixelTransition;
@@ -0,0 +1,115 @@
1
+ import React, { useId, useState } from "react";
2
+ import { AnimatePresence, motion } from "framer-motion";
3
+ import { Check, Sparkles } from "lucide-react";
4
+
5
+ /*
6
+ * Pricing Cards: plans side by side with a monthly / yearly switch. Prices
7
+ * slide to their new value, the featured plan glows, and every card lifts
8
+ * on hover. Wraps to one column on small screens.
9
+ */
10
+
11
+ const DEFAULT_PLANS = [
12
+ { name: "Starter", monthly: 0, yearly: 0, description: "For side projects and trying things out.", features: ["10 components", "Community support", "MIT licence"], cta: "Start free" },
13
+ { name: "Pro", monthly: 19, yearly: 15, description: "For freelancers shipping client work.", features: ["All 60+ components", "Figma kit", "Priority support", "Lifetime updates"], cta: "Go Pro", featured: true },
14
+ { name: "Team", monthly: 49, yearly: 39, description: "For product teams that move fast.", features: ["Everything in Pro", "5 seats", "Private Slack", "Custom requests"], cta: "Contact sales" },
15
+ ];
16
+
17
+ function Price({ value, currency }) {
18
+ return (
19
+ <span className="relative inline-flex h-[1.1em] overflow-hidden align-bottom tabular-nums">
20
+ <AnimatePresence mode="popLayout" initial={false}>
21
+ <motion.span
22
+ key={value}
23
+ initial={{ y: "100%", opacity: 0 }}
24
+ animate={{ y: 0, opacity: 1 }}
25
+ exit={{ y: "-100%", opacity: 0 }}
26
+ transition={{ type: "spring", stiffness: 380, damping: 30 }}
27
+ >
28
+ {currency}
29
+ {value}
30
+ </motion.span>
31
+ </AnimatePresence>
32
+ </span>
33
+ );
34
+ }
35
+
36
+ const PricingCards = ({ plans = DEFAULT_PLANS, currency = "$", yearlyNote = "Save 20%", accentColor = "#a3e635", theme = "dark", onSelect, className = "" }) => {
37
+ const id = useId();
38
+ const [yearly, setYearly] = useState(false);
39
+ const dark = theme !== "light";
40
+
41
+ return (
42
+ <div className={`w-full ${dark ? "text-white" : "text-neutral-900"} ${className}`}>
43
+ <div className="mb-8 flex items-center justify-center gap-3 text-sm font-medium">
44
+ <div className={`relative flex rounded-full p-1 ${dark ? "bg-white/[0.06]" : "bg-black/[0.05]"}`} role="radiogroup" aria-label="Billing period">
45
+ {[false, true].map((y) => (
46
+ <button
47
+ key={String(y)}
48
+ type="button"
49
+ role="radio"
50
+ aria-checked={yearly === y}
51
+ onClick={() => setYearly(y)}
52
+ className={`relative rounded-full px-4 py-1.5 transition-colors ${yearly === y ? "text-neutral-950" : dark ? "text-white/60 hover:text-white" : "text-neutral-500 hover:text-neutral-900"}`}
53
+ >
54
+ {yearly === y && <motion.span layoutId={`${id}-period`} className="absolute inset-0 rounded-full" style={{ background: accentColor }} transition={{ type: "spring", stiffness: 480, damping: 36 }} />}
55
+ <span className="relative">{y ? "Yearly" : "Monthly"}</span>
56
+ </button>
57
+ ))}
58
+ </div>
59
+ <span className="rounded-full px-2 py-0.5 text-[0.72rem] font-semibold" style={{ background: `color-mix(in srgb, ${accentColor} 18%, transparent)`, color: dark ? accentColor : `color-mix(in srgb, ${accentColor} 55%, #000)` }}>
60
+ {yearlyNote}
61
+ </span>
62
+ </div>
63
+
64
+ <div className="grid items-stretch gap-4 [grid-template-columns:repeat(auto-fit,minmax(min(100%,12.5rem),1fr))]">
65
+ {plans.map((plan) => {
66
+ const price = yearly ? plan.yearly : plan.monthly;
67
+ return (
68
+ <motion.article
69
+ key={plan.name}
70
+ whileHover={{ y: -6 }}
71
+ transition={{ type: "spring", stiffness: 300, damping: 24 }}
72
+ className={`relative flex flex-col rounded-3xl border p-6 ${dark ? "bg-neutral-950" : "bg-white"} ${plan.featured ? "" : dark ? "border-white/10" : "border-black/10"}`}
73
+ style={plan.featured ? { borderColor: accentColor, boxShadow: `0 0 0 1px ${accentColor}, 0 24px 60px -24px ${accentColor}88` } : undefined}
74
+ >
75
+ {plan.featured && (
76
+ <span className="absolute -top-3 left-6 inline-flex items-center gap-1 rounded-full px-2.5 py-1 text-[0.7rem] font-bold text-neutral-950" style={{ background: accentColor }}>
77
+ <Sparkles className="h-3 w-3" /> Most popular
78
+ </span>
79
+ )}
80
+ <h3 className="text-lg font-semibold">{plan.name}</h3>
81
+ <p className={`mt-1 text-[0.85rem] ${dark ? "text-white/55" : "text-neutral-500"}`}>{plan.description}</p>
82
+ <p className="mt-5 flex items-end gap-1.5">
83
+ <span className="text-4xl font-bold tracking-tight">
84
+ <Price value={price} currency={currency} />
85
+ </span>
86
+ <span className={`mb-1 text-sm ${dark ? "text-white/45" : "text-neutral-400"}`}>/ month</span>
87
+ </p>
88
+ <p className={`h-5 text-[0.75rem] ${dark ? "text-white/40" : "text-neutral-400"}`}>{yearly && price > 0 ? `${currency}${price * 12} billed yearly` : ""}</p>
89
+ <ul className="mt-5 flex-1 space-y-2.5 text-[0.88rem]">
90
+ {plan.features.map((f) => (
91
+ <li key={f} className="flex items-start gap-2.5">
92
+ <span className="mt-0.5 grid h-4 w-4 shrink-0 place-items-center rounded-full" style={{ background: `color-mix(in srgb, ${accentColor} 22%, transparent)` }}>
93
+ <Check className="h-3 w-3" style={{ color: dark ? accentColor : `color-mix(in srgb, ${accentColor} 50%, #000)` }} />
94
+ </span>
95
+ <span className={dark ? "text-white/80" : "text-neutral-700"}>{f}</span>
96
+ </li>
97
+ ))}
98
+ </ul>
99
+ <button
100
+ type="button"
101
+ onClick={() => onSelect?.(plan, yearly ? "yearly" : "monthly")}
102
+ className={`mt-6 h-11 rounded-full text-sm font-semibold transition-[transform,filter] duration-200 hover:brightness-105 active:scale-[0.97] ${plan.featured ? "text-neutral-950" : dark ? "bg-white/[0.08] text-white hover:bg-white/[0.12]" : "bg-black/[0.05] text-neutral-900 hover:bg-black/[0.08]"}`}
103
+ style={plan.featured ? { background: accentColor } : undefined}
104
+ >
105
+ {plan.cta}
106
+ </button>
107
+ </motion.article>
108
+ );
109
+ })}
110
+ </div>
111
+ </div>
112
+ );
113
+ };
114
+
115
+ export default PricingCards;
@@ -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;