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,123 @@
1
+ import React, { useState } from "react";
2
+ import { AnimatePresence, motion, useMotionValue, useTransform } from "framer-motion";
3
+ import { Heart, RotateCcw, X } from "lucide-react";
4
+
5
+ /*
6
+ * Swipe Cards: a deck you swipe left or right. The top card tilts with the
7
+ * drag and shows a LIKE / NOPE stamp; let go past the threshold and it flies
8
+ * off, or use the buttons and arrow keys. The deck refills when it runs out.
9
+ */
10
+
11
+ const IK = "https://ik.imagekit.io/ny6yinyut/apexui/assets";
12
+ const DEFAULT_CARDS = [
13
+ { image: `${IK}/spiderman.png?tr=f-auto,q-80,w-600`, title: "Spider-Man", subtitle: "Queens · 2.3 km away" },
14
+ { image: `${IK}/hulk.png?tr=f-auto,q-80,w-600`, title: "Hulk", subtitle: "New York · 5 km away" },
15
+ { image: `${IK}/black-panther.png?tr=f-auto,q-80,w-600`, title: "Black Panther", subtitle: "Wakanda · 8,412 km away" },
16
+ { image: `${IK}/doctorStrange.png?tr=f-auto,q-80,w-600`, title: "Doctor Strange", subtitle: "Sanctum · 3 km away" },
17
+ { image: `${IK}/captainamerica.png?tr=f-auto,q-80,w-600`, title: "Captain America", subtitle: "Brooklyn · 1 km away" },
18
+ ];
19
+
20
+ function TopCard({ card, onSwipe, threshold, likeColor, nopeColor }) {
21
+ const x = useMotionValue(0);
22
+ const rotate = useTransform(x, [-240, 240], [-18, 18]);
23
+ const likeOpacity = useTransform(x, [20, threshold], [0, 1]);
24
+ const nopeOpacity = useTransform(x, [-threshold, -20], [1, 0]);
25
+
26
+ return (
27
+ <motion.div
28
+ className="absolute inset-0 z-10 cursor-grab touch-none overflow-hidden rounded-3xl bg-neutral-900 shadow-[0_30px_60px_-24px_rgba(0,0,0,0.6)] active:cursor-grabbing"
29
+ style={{ x, rotate }}
30
+ drag="x"
31
+ dragSnapToOrigin
32
+ dragElastic={0.9}
33
+ onDragEnd={(_, info) => {
34
+ if (info.offset.x > threshold || info.velocity.x > 700) onSwipe("right");
35
+ else if (info.offset.x < -threshold || info.velocity.x < -700) onSwipe("left");
36
+ }}
37
+ exit={(dir) => ({ x: dir === "right" ? 520 : -520, rotate: dir === "right" ? 26 : -26, opacity: 0, transition: { duration: 0.35, ease: [0.22, 1, 0.36, 1] } })}
38
+ >
39
+ <img src={card.image} alt="" draggable="false" className="pointer-events-none h-full w-full object-cover" />
40
+ <div className="pointer-events-none absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/85 via-black/30 to-transparent p-5 pt-16 text-white">
41
+ <p className="text-xl font-bold">{card.title}</p>
42
+ {card.subtitle && <p className="text-sm text-white/70">{card.subtitle}</p>}
43
+ </div>
44
+ <motion.span className="pointer-events-none absolute left-5 top-6 -rotate-12 rounded-lg border-[3px] px-3 py-1 text-2xl font-black tracking-widest" style={{ opacity: likeOpacity, color: likeColor, borderColor: likeColor }}>
45
+ LIKE
46
+ </motion.span>
47
+ <motion.span className="pointer-events-none absolute right-5 top-6 rotate-12 rounded-lg border-[3px] px-3 py-1 text-2xl font-black tracking-widest" style={{ opacity: nopeOpacity, color: nopeColor, borderColor: nopeColor }}>
48
+ NOPE
49
+ </motion.span>
50
+ </motion.div>
51
+ );
52
+ }
53
+
54
+ const SwipeCards = ({ cards = DEFAULT_CARDS, threshold = 110, likeColor = "#a3e635", nopeColor = "#f43f5e", onSwipe, className = "" }) => {
55
+ const [index, setIndex] = useState(0);
56
+ const [dir, setDir] = useState("right");
57
+ const [history, setHistory] = useState([]);
58
+ const visible = [0, 1, 2].map((o) => cards[(index + o) % cards.length]);
59
+
60
+ const swipe = (direction) => {
61
+ const card = cards[index % cards.length];
62
+ setDir(direction);
63
+ setHistory((h) => [...h, { card, direction }]);
64
+ onSwipe?.(card, direction);
65
+ setIndex((i) => i + 1);
66
+ };
67
+ const undo = () => {
68
+ if (!history.length) return;
69
+ setDir("left");
70
+ setHistory((h) => h.slice(0, -1));
71
+ setIndex((i) => Math.max(0, i - 1));
72
+ };
73
+
74
+ return (
75
+ <div
76
+ className={`flex flex-col items-center outline-none ${className}`}
77
+ tabIndex={0}
78
+ onKeyDown={(e) => {
79
+ if (e.key === "ArrowRight") swipe("right");
80
+ if (e.key === "ArrowLeft") swipe("left");
81
+ }}
82
+ aria-label="Card deck: use the arrow keys or the buttons to swipe"
83
+ >
84
+ <div className="relative aspect-[3/4] w-[min(18rem,78vw)]">
85
+ {/* Cards waiting underneath, slightly smaller and lower. */}
86
+ {visible
87
+ .slice(1)
88
+ .reverse()
89
+ .map((card, i, arr) => {
90
+ const depth = arr.length - i;
91
+ return (
92
+ <motion.div
93
+ key={`${index + depth}-${card.title}`}
94
+ className="absolute inset-0 overflow-hidden rounded-3xl bg-neutral-900"
95
+ initial={false}
96
+ animate={{ scale: 1 - depth * 0.05, y: depth * 14, opacity: 1 - depth * 0.25 }}
97
+ transition={{ type: "spring", stiffness: 300, damping: 28 }}
98
+ >
99
+ <img src={card.image} alt="" draggable="false" className="h-full w-full object-cover" />
100
+ </motion.div>
101
+ );
102
+ })}
103
+ <AnimatePresence custom={dir} initial={false}>
104
+ <TopCard key={`${index}-${visible[0].title}`} card={visible[0]} onSwipe={swipe} threshold={threshold} likeColor={likeColor} nopeColor={nopeColor} />
105
+ </AnimatePresence>
106
+ </div>
107
+
108
+ <div className="mt-8 flex items-center gap-4">
109
+ <motion.button whileTap={{ scale: 0.88 }} whileHover={{ scale: 1.06 }} type="button" aria-label="Nope" onClick={() => swipe("left")} className="grid h-14 w-14 place-items-center rounded-full border-2 bg-black/5 shadow-lg" style={{ borderColor: nopeColor, color: nopeColor }}>
110
+ <X className="h-6 w-6" strokeWidth={3} />
111
+ </motion.button>
112
+ <motion.button whileTap={{ scale: 0.88 }} whileHover={{ scale: 1.06 }} type="button" aria-label="Undo" onClick={undo} disabled={!history.length} className="grid h-10 w-10 place-items-center rounded-full border border-current/20 opacity-70 disabled:opacity-30">
113
+ <RotateCcw className="h-4 w-4" />
114
+ </motion.button>
115
+ <motion.button whileTap={{ scale: 0.88 }} whileHover={{ scale: 1.06 }} type="button" aria-label="Like" onClick={() => swipe("right")} className="grid h-14 w-14 place-items-center rounded-full border-2 bg-black/5 shadow-lg" style={{ borderColor: likeColor, color: likeColor }}>
116
+ <Heart className="h-6 w-6" fill="currentColor" />
117
+ </motion.button>
118
+ </div>
119
+ </div>
120
+ );
121
+ };
122
+
123
+ export default SwipeCards;
@@ -0,0 +1,73 @@
1
+ import React, { useRef } from "react";
2
+ import { useAnimationFrame, useReducedMotion } from "framer-motion";
3
+
4
+ /*
5
+ * 3D Text Ring: letters stand on a ring in real 3D space and the ring
6
+ * turns, so text sweeps around the back and comes forward again. Letters
7
+ * fade and soften as they pass behind. Drag to spin it yourself.
8
+ */
9
+
10
+ const TextRing3D = ({ text = "APEX UI · 3D TYPE · ", radius = 9, speed = 30, tilt = -14, fontSize = 2, color = "currentColor", accentColor = "#a3e635", className = "" }) => {
11
+ const reduced = useReducedMotion();
12
+ const ring = useRef(null);
13
+ const letters = useRef([]);
14
+ const angle = useRef(0);
15
+ const vel = useRef(0);
16
+ const drag = useRef(null);
17
+ const chars = Array.from(text);
18
+ const step = 360 / chars.length;
19
+
20
+ useAnimationFrame((_, delta) => {
21
+ const dt = Math.min(0.05, delta / 1000);
22
+ if (!drag.current) {
23
+ vel.current *= 0.95;
24
+ angle.current += (reduced ? 0 : speed) * dt + vel.current * dt;
25
+ }
26
+ if (ring.current) ring.current.style.transform = `rotateX(${tilt}deg) rotateY(${-angle.current}deg)`;
27
+ letters.current.forEach((el, i) => {
28
+ if (!el) return;
29
+ // How much this letter faces the viewer (1 front, -1 back).
30
+ const facing = Math.cos(((i * step - angle.current) * Math.PI) / 180);
31
+ el.style.opacity = String(0.18 + 0.82 * ((facing + 1) / 2));
32
+ el.style.color = facing > 0.9 ? accentColor : "";
33
+ });
34
+ });
35
+
36
+ return (
37
+ <div
38
+ className={`grid cursor-grab touch-none select-none place-items-center active:cursor-grabbing [perspective:900px] ${className}`}
39
+ style={{ width: `${radius * 2.4}rem`, maxWidth: "100%", height: `${fontSize * 3.2 + 4}rem`, color }}
40
+ onPointerDown={(e) => {
41
+ e.currentTarget.setPointerCapture(e.pointerId);
42
+ drag.current = { x: e.clientX, a: angle.current, t: performance.now() };
43
+ }}
44
+ onPointerMove={(e) => {
45
+ const d = drag.current;
46
+ if (!d) return;
47
+ const now = performance.now();
48
+ const next = d.a - (e.clientX - d.x) * 0.4;
49
+ vel.current = ((next - angle.current) / Math.max(1, now - d.t)) * 1000;
50
+ d.t = now;
51
+ angle.current = next;
52
+ }}
53
+ onPointerUp={() => (drag.current = null)}
54
+ aria-label={text}
55
+ role="img"
56
+ >
57
+ <div ref={ring} className="relative h-0 w-0 [transform-style:preserve-3d]">
58
+ {chars.map((c, i) => (
59
+ <span
60
+ key={i}
61
+ ref={(el) => (letters.current[i] = el)}
62
+ className="absolute left-0 top-0 -translate-x-1/2 -translate-y-1/2 font-black leading-none transition-colors duration-300"
63
+ style={{ fontSize: `${fontSize}rem`, transform: `rotateY(${i * step}deg) translateZ(${radius}rem)` }}
64
+ >
65
+ {c === " " ? " " : c}
66
+ </span>
67
+ ))}
68
+ </div>
69
+ </div>
70
+ );
71
+ };
72
+
73
+ export default TextRing3D;
@@ -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}
@@ -0,0 +1,64 @@
1
+ import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
2
+ import { motion } from "framer-motion";
3
+
4
+ /*
5
+ * True Focus: one word of a sentence is sharp while the rest are blurred,
6
+ * and a set of corner brackets glides from word to word. Cycles on its
7
+ * own, or follows the pointer in manual mode.
8
+ */
9
+
10
+ const TrueFocus = ({ sentence = "Design Build Ship Repeat", manual = false, blur = 5, interval = 1600, borderColor = "#a3e635", className = "" }) => {
11
+ const words = sentence.split(" ");
12
+ const [active, setActive] = useState(0);
13
+ const [box, setBox] = useState(null);
14
+ const wrap = useRef(null);
15
+ const refs = useRef([]);
16
+
17
+ useEffect(() => {
18
+ if (manual) return undefined;
19
+ const id = setInterval(() => setActive((i) => (i + 1) % words.length), interval);
20
+ return () => clearInterval(id);
21
+ }, [manual, interval, words.length]);
22
+
23
+ useLayoutEffect(() => {
24
+ const measure = () => {
25
+ const el = refs.current[active];
26
+ const parent = wrap.current;
27
+ if (!el || !parent) return;
28
+ const a = el.getBoundingClientRect();
29
+ const p = parent.getBoundingClientRect();
30
+ setBox({ x: a.left - p.left, y: a.top - p.top, w: a.width, h: a.height });
31
+ };
32
+ measure();
33
+ const ro = new ResizeObserver(measure);
34
+ ro.observe(wrap.current);
35
+ return () => ro.disconnect();
36
+ }, [active, sentence]);
37
+
38
+ const corner = "absolute h-[0.42em] w-[0.42em] border-[0.09em]";
39
+ return (
40
+ <div ref={wrap} className={`relative inline-flex flex-wrap justify-center gap-x-[0.45em] gap-y-2 font-black ${className}`}>
41
+ {words.map((w, i) => (
42
+ <span
43
+ key={i}
44
+ ref={(el) => (refs.current[i] = el)}
45
+ onPointerEnter={() => manual && setActive(i)}
46
+ className="relative px-[0.12em] transition-[filter,opacity] duration-500"
47
+ style={{ filter: i === active ? "blur(0px)" : `blur(${blur}px)`, opacity: i === active ? 1 : 0.55 }}
48
+ >
49
+ {w}
50
+ </span>
51
+ ))}
52
+ {box && (
53
+ <motion.div aria-hidden="true" className="pointer-events-none absolute left-0 top-0" initial={false} animate={{ x: box.x - 6, y: box.y - 4, width: box.w + 12, height: box.h + 8 }} transition={{ type: "spring", stiffness: 260, damping: 28 }}>
54
+ <span className={`${corner} -left-0 -top-0 rounded-tl-[0.12em] border-b-0 border-r-0`} style={{ borderColor, filter: `drop-shadow(0 0 6px ${borderColor})` }} />
55
+ <span className={`${corner} -right-0 -top-0 rounded-tr-[0.12em] border-b-0 border-l-0`} style={{ borderColor, filter: `drop-shadow(0 0 6px ${borderColor})` }} />
56
+ <span className={`${corner} -bottom-0 -left-0 rounded-bl-[0.12em] border-r-0 border-t-0`} style={{ borderColor, filter: `drop-shadow(0 0 6px ${borderColor})` }} />
57
+ <span className={`${corner} -bottom-0 -right-0 rounded-br-[0.12em] border-l-0 border-t-0`} style={{ borderColor, filter: `drop-shadow(0 0 6px ${borderColor})` }} />
58
+ </motion.div>
59
+ )}
60
+ </div>
61
+ );
62
+ };
63
+
64
+ export default TrueFocus;
@@ -0,0 +1,70 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { useReducedMotion } from "framer-motion";
3
+
4
+ /*
5
+ * Typewriter Text: types a phrase, pauses, deletes it and moves on to the
6
+ * next, with a blinking caret. Put static text before or after with
7
+ * `prefix` / `suffix`; only the rotating words are typed.
8
+ */
9
+
10
+ const TypewriterText = ({
11
+ words = ["beautiful interfaces.", "smooth animations.", "accessible components.", "faster launches."],
12
+ prefix = "We build ",
13
+ suffix = "",
14
+ typeSpeed = 70,
15
+ deleteSpeed = 40,
16
+ pause = 1600,
17
+ loop = true,
18
+ caret = "bar",
19
+ accentColor = "#a3e635",
20
+ as: Tag = "p",
21
+ className = "",
22
+ }) => {
23
+ const reduced = useReducedMotion();
24
+ const [index, setIndex] = useState(0);
25
+ const [text, setText] = useState("");
26
+ const [deleting, setDeleting] = useState(false);
27
+ const word = words[index % words.length] ?? "";
28
+
29
+ useEffect(() => {
30
+ if (reduced) return undefined;
31
+ let delay = deleting ? deleteSpeed : typeSpeed + Math.random() * 40;
32
+ if (!deleting && text === word) {
33
+ if (!loop && index === words.length - 1) return undefined;
34
+ delay = pause;
35
+ } else if (deleting && text === "") delay = 300;
36
+
37
+ const id = setTimeout(() => {
38
+ if (!deleting && text === word) setDeleting(true);
39
+ else if (deleting && text === "") {
40
+ setDeleting(false);
41
+ setIndex((i) => (i + 1) % words.length);
42
+ } else setText(word.slice(0, text.length + (deleting ? -1 : 1)));
43
+ }, delay);
44
+ return () => clearTimeout(id);
45
+ }, [text, deleting, word, index, words.length, typeSpeed, deleteSpeed, pause, loop, reduced]);
46
+
47
+ const shown = reduced ? word : text;
48
+ const caretEl =
49
+ caret === "none" ? null : (
50
+ <span
51
+ aria-hidden="true"
52
+ className={`ml-[0.06em] inline-block animate-[apex-caret_1s_steps(1)_infinite] align-baseline ${caret === "block" ? "h-[1em] w-[0.55em] translate-y-[0.12em]" : caret === "underscore" ? "h-[0.12em] w-[0.6em]" : "h-[1em] w-[0.08em] translate-y-[0.12em]"}`}
53
+ style={{ background: accentColor }}
54
+ />
55
+ );
56
+
57
+ return (
58
+ <Tag className={className} aria-label={`${prefix}${words.join(", ")}${suffix}`}>
59
+ <style>{"@keyframes apex-caret{0%,100%{opacity:1}50%{opacity:0}}"}</style>
60
+ <span aria-hidden="true">
61
+ {prefix}
62
+ <span style={{ color: accentColor }}>{shown}</span>
63
+ {caretEl}
64
+ {suffix}
65
+ </span>
66
+ </Tag>
67
+ );
68
+ };
69
+
70
+ export default TypewriterText;