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,53 @@
1
+ import React, { useState } from "react";
2
+ import { motion } from "framer-motion";
3
+
4
+ /*
5
+ * Bounce Cards: a hand of photo cards that bounce into a fan on mount.
6
+ * Hover one and it straightens and lifts while its neighbours lean away to
7
+ * make room; move off and everything settles back into the fan.
8
+ */
9
+
10
+ const IK = "https://ik.imagekit.io/ny6yinyut/apexui/assets";
11
+ const img = (n) => `${IK}/${n}.png?tr=f-auto,q-80,w-400`;
12
+ const DEFAULT_IMAGES = [img("hulk"), img("captainamerica"), img("spiderman"), img("doctorStrange"), img("black-panther")];
13
+
14
+ const BounceCards = ({ images = DEFAULT_IMAGES, cardSize = 9, spread = 5.5, fan = 8, delay = 0.2, className = "" }) => {
15
+ const [hovered, setHovered] = useState(null);
16
+ const mid = (images.length - 1) / 2;
17
+
18
+ return (
19
+ <div className={`relative flex items-center justify-center ${className}`} style={{ width: `min(${cardSize + spread * (images.length - 1) + 4}rem, 92vw)`, height: `${cardSize * 1.5}rem` }} onPointerLeave={() => setHovered(null)}>
20
+ {images.map((src, i) => {
21
+ const off = i - mid;
22
+ let x = off * spread;
23
+ let rotate = off * fan;
24
+ let y = Math.abs(off) * 0.6;
25
+ if (hovered !== null) {
26
+ if (i === hovered) {
27
+ rotate = 0;
28
+ y = -1;
29
+ } else {
30
+ // Neighbours lean away, closer ones more.
31
+ const d = i - hovered;
32
+ x += Math.sign(d) * Math.max(0, 3 - Math.abs(d) * 0.8);
33
+ }
34
+ }
35
+ return (
36
+ <motion.div
37
+ key={i}
38
+ className="absolute overflow-hidden rounded-[1.4rem] border-[0.35rem] border-white bg-white shadow-[0_18px_40px_-18px_rgba(0,0,0,0.55)]"
39
+ style={{ width: `${cardSize}rem`, height: `${cardSize}rem`, zIndex: i === hovered ? 50 : 10 - Math.abs(Math.round(off)) }}
40
+ initial={{ scale: 0, x: 0, y: "0rem", rotate: 0 }}
41
+ animate={{ scale: i === hovered ? 1.06 : 1, x: `${x}rem`, y: `${y}rem`, rotate }}
42
+ transition={{ type: "spring", stiffness: 260, damping: 14, delay: hovered === null ? delay + i * 0.06 : 0 }}
43
+ onPointerEnter={() => setHovered(i)}
44
+ >
45
+ <img src={src} alt="" draggable="false" className="h-full w-full bg-neutral-200 object-cover" />
46
+ </motion.div>
47
+ );
48
+ })}
49
+ </div>
50
+ );
51
+ };
52
+
53
+ export default BounceCards;
@@ -0,0 +1,80 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { motion } from "framer-motion";
3
+ import { Code2, Layers, Sparkles, Zap } from "lucide-react";
4
+
5
+ /*
6
+ * Card Swap 3D: cards stacked diagonally into the distance in 3D. On a
7
+ * timer the front card drops away and slides to the back of the stack
8
+ * while the rest step forward. Pauses while hovered; click to swap now.
9
+ */
10
+
11
+ const DEFAULT_CARDS = [
12
+ { icon: Sparkles, title: "Smooth", body: "Springs tuned to feel natural, never floaty." },
13
+ { icon: Layers, title: "Composable", body: "Drop components in and style them with Tailwind." },
14
+ { icon: Code2, title: "Yours", body: "Copy the source or add it with one CLI command." },
15
+ { icon: Zap, title: "Fast", body: "Transforms and opacity only — 60fps on phones." },
16
+ ];
17
+
18
+ const CardSwap3D = ({ cards = DEFAULT_CARDS, delay = 3200, distanceX = 3.6, distanceY = 3.2, skew = 5, pauseOnHover = true, accentColor = "#a3e635", theme = "dark", width = 18, className = "" }) => {
19
+ // order[0] is the card in front.
20
+ const [order, setOrder] = useState(() => cards.map((_, i) => i));
21
+ const [dropping, setDropping] = useState(null);
22
+ const [paused, setPaused] = useState(false);
23
+ const dark = theme !== "light";
24
+
25
+ const swap = () => {
26
+ if (dropping !== null) return;
27
+ setDropping(order[0]);
28
+ setTimeout(() => {
29
+ setOrder((o) => [...o.slice(1), o[0]]);
30
+ setDropping(null);
31
+ }, 520);
32
+ };
33
+
34
+ useEffect(() => {
35
+ if (paused) return undefined;
36
+ const id = setInterval(swap, delay);
37
+ return () => clearInterval(id);
38
+ });
39
+
40
+ return (
41
+ <div
42
+ className={`relative [perspective:1000px] ${className}`}
43
+ style={{ width: `min(${width}rem, 70vw)`, height: `${width * 0.68}rem`, marginTop: `${distanceY * (cards.length - 1)}rem`, marginRight: `${distanceX * (cards.length - 1)}rem` }}
44
+ onPointerEnter={() => pauseOnHover && setPaused(true)}
45
+ onPointerLeave={() => setPaused(false)}
46
+ onClick={swap}
47
+ >
48
+ {cards.map((card, i) => {
49
+ const slot = order.indexOf(i);
50
+ const isDropping = dropping === i;
51
+ const Icon = card.icon ?? Sparkles;
52
+ return (
53
+ <motion.div
54
+ key={i}
55
+ className={`absolute inset-0 cursor-pointer overflow-hidden rounded-2xl border p-6 [backface-visibility:hidden] ${dark ? "border-white/10 bg-neutral-950 text-white" : "border-black/10 bg-white text-neutral-900"}`}
56
+ style={{ zIndex: isDropping ? 60 : cards.length - slot, transformStyle: "preserve-3d", boxShadow: "0 25px 50px -25px rgba(0,0,0,0.6)" }}
57
+ initial={false}
58
+ animate={
59
+ isDropping
60
+ ? { x: "0rem", y: "14rem", z: 0, skewY: skew, opacity: 0.0 }
61
+ : { x: `${slot * distanceX}rem`, y: `${-slot * distanceY}rem`, z: -slot * 60, skewY: skew, opacity: slot > 4 ? 0 : 1 }
62
+ }
63
+ transition={isDropping ? { duration: 0.5, ease: [0.55, 0, 0.8, 0.2] } : { type: "spring", stiffness: 140, damping: 20 }}
64
+ >
65
+ <div className="flex items-center gap-2.5">
66
+ <span className="grid h-9 w-9 place-items-center rounded-xl text-black" style={{ background: accentColor }}>
67
+ <Icon className="h-4.5 w-4.5" />
68
+ </span>
69
+ <p className="text-lg font-semibold">{card.title}</p>
70
+ </div>
71
+ <p className={`mt-3 text-[0.92rem] leading-relaxed ${dark ? "text-white/60" : "text-neutral-500"}`}>{card.body}</p>
72
+ <div className="pointer-events-none absolute -bottom-10 -right-10 h-32 w-32 rounded-full opacity-20 blur-2xl" style={{ background: accentColor }} />
73
+ </motion.div>
74
+ );
75
+ })}
76
+ </div>
77
+ );
78
+ };
79
+
80
+ export default CardSwap3D;
@@ -0,0 +1,222 @@
1
+ import React, { useEffect, useRef, useState } from "react";
2
+ import { AnimatePresence, animate, motion, useAnimationFrame, useMotionValue, useMotionValueEvent, useReducedMotion, useTransform } from "framer-motion";
3
+ import { ChevronLeft, ChevronRight } from "lucide-react";
4
+
5
+ /*
6
+ * 3D Carousel: cards stand on a ring that turns in 3D. Drag (or swipe) to
7
+ * spin it, let go and it settles on the nearest card. It turns slowly on its
8
+ * own, and a click on a card or the arrows brings that card to the front.
9
+ */
10
+
11
+ const IK = "https://ik.imagekit.io/ny6yinyut/apexui/assets";
12
+ const img = (name) => `${IK}/${name}?tr=f-auto,q-80,w-480`;
13
+
14
+ const DEFAULT_ITEMS = [
15
+ { image: img("ironman.png"), title: "Iron Man", subtitle: "Genius, billionaire" },
16
+ { image: img("spiderman.png"), title: "Spider-Man", subtitle: "Friendly neighbour" },
17
+ { image: img("captainamerica.png"), title: "Captain America", subtitle: "The first avenger" },
18
+ { image: img("hulk.png"), title: "Hulk", subtitle: "Always angry" },
19
+ { image: img("black-panther.png"), title: "Black Panther", subtitle: "King of Wakanda" },
20
+ { image: img("doctorStrange.png"), title: "Doctor Strange", subtitle: "Sorcerer supreme" },
21
+ { image: img("thanos.png"), title: "Thanos", subtitle: "Inevitable" },
22
+ ];
23
+
24
+ const settle = { type: "spring", stiffness: 140, damping: 22, mass: 1 };
25
+ const mod = (n, m) => ((n % m) + m) % m;
26
+
27
+ function Card({ item, index, step, radius, rotation, onSelect }) {
28
+ // Cards turned away from the viewer get darker.
29
+ const brightness = useTransform(rotation, (r) => {
30
+ const facing = Math.cos(((index * step + r) * Math.PI) / 180);
31
+ return `brightness(${0.35 + 0.65 * Math.max(0, facing)})`;
32
+ });
33
+
34
+ return (
35
+ <motion.button
36
+ type="button"
37
+ aria-label={item.title}
38
+ onClick={() => onSelect(index)}
39
+ className="absolute inset-0 overflow-hidden rounded-[1.1rem] bg-neutral-900 text-left text-white shadow-[0_24px_50px_-20px_rgba(0,0,0,0.6)] outline-none [backface-visibility:hidden] focus-visible:ring-2 focus-visible:ring-white/70"
40
+ style={{ transform: `rotateY(${index * step}deg) translateZ(${radius}px)`, filter: brightness }}
41
+ >
42
+ <img src={item.image} alt="" draggable="false" className="pointer-events-none h-full w-full select-none object-cover" />
43
+ <span className="pointer-events-none absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/85 via-black/40 to-transparent px-3.5 pb-3 pt-10">
44
+ <span className="block truncate text-[0.95rem] font-semibold">{item.title}</span>
45
+ {item.subtitle && <span className="block truncate text-[0.75rem] text-white/65">{item.subtitle}</span>}
46
+ </span>
47
+ <span className="pointer-events-none absolute inset-0 rounded-[1.1rem] ring-1 ring-inset ring-white/10" />
48
+ </motion.button>
49
+ );
50
+ }
51
+
52
+ const Carousel3D = ({
53
+ items = DEFAULT_ITEMS,
54
+ cardWidth = 11,
55
+ aspectRatio = "3 / 4",
56
+ gap = 1,
57
+ tilt = -6,
58
+ perspective = 1400,
59
+ autoRotate = true,
60
+ autoRotateSpeed = 10,
61
+ pauseOnHover = true,
62
+ showControls = true,
63
+ onActiveChange,
64
+ className = "",
65
+ }) => {
66
+ const count = Math.max(items.length, 1);
67
+ const step = 360 / count;
68
+ const rotation = useMotionValue(0);
69
+ const reduced = useReducedMotion();
70
+ const cardRef = useRef(null);
71
+ const drag = useRef(null);
72
+ const dragged = useRef(false);
73
+ const busy = useRef(false);
74
+ const [radius, setRadius] = useState(300);
75
+ const [hovered, setHovered] = useState(false);
76
+ const [active, setActive] = useState(0);
77
+ const onActiveRef = useRef(onActiveChange);
78
+ onActiveRef.current = onActiveChange;
79
+
80
+ // Ring radius that fits the cards side by side, from their real width.
81
+ useEffect(() => {
82
+ const el = cardRef.current;
83
+ if (!el) return undefined;
84
+ const measure = () => {
85
+ const w = el.offsetWidth;
86
+ const gapPx = gap * parseFloat(getComputedStyle(document.documentElement).fontSize || "16");
87
+ setRadius(Math.round(w / 2 / Math.tan(Math.PI / count) + gapPx));
88
+ };
89
+ measure();
90
+ const ro = new ResizeObserver(measure);
91
+ ro.observe(el);
92
+ return () => ro.disconnect();
93
+ }, [count, gap]);
94
+
95
+ useMotionValueEvent(rotation, "change", (r) => {
96
+ const next = mod(Math.round(-r / step), count);
97
+ setActive((prev) => {
98
+ if (prev !== next) onActiveRef.current?.(next, items[next]);
99
+ return next;
100
+ });
101
+ });
102
+
103
+ // Slow idle spin.
104
+ useAnimationFrame((_, delta) => {
105
+ if (!autoRotate || reduced || drag.current || busy.current || (pauseOnHover && hovered)) return;
106
+ rotation.set(rotation.get() - (autoRotateSpeed * Math.min(delta, 64)) / 1000);
107
+ });
108
+
109
+ const spinTo = (target, velocity = 0) => {
110
+ busy.current = true;
111
+ animate(rotation, target, { ...settle, velocity, onComplete: () => (busy.current = false) });
112
+ };
113
+
114
+ const goTo = (index) => {
115
+ const current = rotation.get();
116
+ const diff = mod(-index * step - current + 180, 360) - 180;
117
+ spinTo(current + diff);
118
+ };
119
+
120
+ const onPointerDown = (e) => {
121
+ if (e.button !== undefined && e.button !== 0) return;
122
+ rotation.stop();
123
+ busy.current = false;
124
+ drag.current = { x: e.clientX, start: rotation.get(), moved: false, id: e.pointerId };
125
+ };
126
+ const onPointerMove = (e) => {
127
+ const d = drag.current;
128
+ if (!d) return;
129
+ const dx = e.clientX - d.x;
130
+ if (!d.moved && Math.abs(dx) > 5) {
131
+ d.moved = true;
132
+ e.currentTarget.setPointerCapture?.(d.id);
133
+ }
134
+ if (d.moved) rotation.set(d.start + dx * (180 / (Math.PI * radius)));
135
+ };
136
+ const onPointerUp = () => {
137
+ const d = drag.current;
138
+ drag.current = null;
139
+ dragged.current = Boolean(d?.moved);
140
+ if (!d?.moved) return;
141
+ setTimeout(() => (dragged.current = false), 0);
142
+ const velocity = rotation.getVelocity();
143
+ const projected = rotation.get() + velocity * 0.25;
144
+ spinTo(Math.round(projected / step) * step, velocity);
145
+ };
146
+
147
+ const onKeyDown = (e) => {
148
+ if (e.key === "ArrowRight") goTo(active + 1);
149
+ if (e.key === "ArrowLeft") goTo(active - 1);
150
+ };
151
+
152
+ const current = items[active];
153
+ const roundBtn = "grid h-10 w-10 place-items-center rounded-full border backdrop-blur transition hover:scale-105 active:scale-95";
154
+ const btnStyle = { borderColor: "color-mix(in srgb, currentColor 18%, transparent)", background: "color-mix(in srgb, currentColor 6%, transparent)" };
155
+
156
+ return (
157
+ <section
158
+ aria-roledescription="carousel"
159
+ aria-label="3D carousel"
160
+ className={`flex w-full flex-col items-center ${className}`}
161
+ onPointerEnter={(e) => e.pointerType === "mouse" && setHovered(true)}
162
+ onPointerLeave={() => setHovered(false)}
163
+ >
164
+ <div
165
+ tabIndex={0}
166
+ onKeyDown={onKeyDown}
167
+ onPointerDown={onPointerDown}
168
+ onPointerMove={onPointerMove}
169
+ onPointerUp={onPointerUp}
170
+ onPointerCancel={onPointerUp}
171
+ onClickCapture={(e) => {
172
+ // A drag should not count as a click on the card under the pointer.
173
+ if (dragged.current) e.stopPropagation();
174
+ }}
175
+ className="relative flex w-full cursor-grab touch-pan-y select-none items-center justify-center rounded-2xl py-8 outline-none active:cursor-grabbing focus-visible:ring-2 focus-visible:ring-current/30"
176
+ style={{ perspective: `${perspective}px` }}
177
+ >
178
+ {/* Push the ring back by its radius so the front card sits at its real size. */}
179
+ <div style={{ transformStyle: "preserve-3d", transform: `translateZ(-${radius}px) rotateX(${tilt}deg)` }}>
180
+ <motion.div
181
+ ref={cardRef}
182
+ className="relative"
183
+ style={{ width: `min(${cardWidth}rem, 40vw)`, aspectRatio, transformStyle: "preserve-3d", rotateY: rotation }}
184
+ >
185
+ {items.map((item, i) => (
186
+ <Card key={i} item={item} index={i} step={step} radius={radius} rotation={rotation} onSelect={goTo} />
187
+ ))}
188
+ </motion.div>
189
+ </div>
190
+ </div>
191
+
192
+ {showControls && (
193
+ <div className="mt-2 flex items-center gap-5">
194
+ <button type="button" aria-label="Previous" onClick={() => goTo(active - 1)} className={roundBtn} style={btnStyle}>
195
+ <ChevronLeft className="h-5 w-5" />
196
+ </button>
197
+ <div className="min-w-[9rem] text-center" aria-live="polite">
198
+ <AnimatePresence mode="wait" initial={false}>
199
+ <motion.div
200
+ key={active}
201
+ initial={{ opacity: 0, y: 6 }}
202
+ animate={{ opacity: 1, y: 0 }}
203
+ exit={{ opacity: 0, y: -6 }}
204
+ transition={{ duration: 0.18 }}
205
+ >
206
+ <p className="text-[0.95rem] font-semibold">{current?.title}</p>
207
+ <p className="text-[0.75rem] opacity-60">
208
+ {active + 1} / {count}
209
+ </p>
210
+ </motion.div>
211
+ </AnimatePresence>
212
+ </div>
213
+ <button type="button" aria-label="Next" onClick={() => goTo(active + 1)} className={roundBtn} style={btnStyle}>
214
+ <ChevronRight className="h-5 w-5" />
215
+ </button>
216
+ </div>
217
+ )}
218
+ </section>
219
+ );
220
+ };
221
+
222
+ export default Carousel3D;
@@ -0,0 +1,49 @@
1
+ import React, { useRef } from "react";
2
+ import { useAnimationFrame, useReducedMotion } from "framer-motion";
3
+
4
+ /*
5
+ * Circular Text: a line of text wrapped around a circle that keeps turning.
6
+ * On hover it can speed up, slow down, pause or spin wildly. Put anything in
7
+ * the middle with children.
8
+ */
9
+
10
+ const HOVER_SPEED = { speedUp: 4, slowDown: 0.25, pause: 0, goBonkers: 14, none: 1 };
11
+
12
+ const CircularText = ({ text = "APEX UI • MOTION • COMPONENTS • ", size = 11, duration = 18, onHover = "speedUp", color = "currentColor", children, className = "" }) => {
13
+ const reduced = useReducedMotion();
14
+ const ring = useRef(null);
15
+ const angle = useRef(0);
16
+ const speed = useRef(1);
17
+ const target = useRef(1);
18
+ const chars = Array.from(text);
19
+
20
+ useAnimationFrame((_, delta) => {
21
+ if (reduced || !ring.current) return;
22
+ speed.current += (target.current - speed.current) * 0.06;
23
+ angle.current = (angle.current + (360 / duration) * (delta / 1000) * speed.current) % 360;
24
+ const s = onHover === "goBonkers" && target.current > 1 ? 1 + Math.min(0.12, (speed.current - 1) * 0.01) : 1;
25
+ ring.current.style.transform = `rotate(${angle.current}deg) scale(${s})`;
26
+ });
27
+
28
+ return (
29
+ <div
30
+ className={`relative grid place-items-center rounded-full ${className}`}
31
+ style={{ width: `${size}rem`, height: `${size}rem`, color }}
32
+ onPointerEnter={() => (target.current = HOVER_SPEED[onHover] ?? 1)}
33
+ onPointerLeave={() => (target.current = 1)}
34
+ aria-label={text}
35
+ role="img"
36
+ >
37
+ <div ref={ring} className="absolute inset-0" aria-hidden="true">
38
+ {chars.map((c, i) => (
39
+ <span key={i} className="absolute left-1/2 top-0 h-1/2 origin-bottom text-[0.95rem] font-bold uppercase" style={{ transform: `translateX(-50%) rotate(${(i / chars.length) * 360}deg)` }}>
40
+ {c}
41
+ </span>
42
+ ))}
43
+ </div>
44
+ {children && <div className="relative">{children}</div>}
45
+ </div>
46
+ );
47
+ };
48
+
49
+ export default CircularText;
@@ -0,0 +1,72 @@
1
+ import React, { useEffect, useRef } from "react";
2
+
3
+ /*
4
+ * Click Spark: wrap anything and every click throws out a ring of short
5
+ * spark lines from the pointer that shoot out and fade. Drawn on a canvas
6
+ * overlay so it never blocks clicks underneath.
7
+ */
8
+
9
+ const ease = (t) => t * (2 - t);
10
+
11
+ const ClickSpark = ({ sparkColor = "#a3e635", sparkCount = 10, sparkRadius = 26, sparkSize = 12, duration = 450, children, className = "" }) => {
12
+ const wrap = useRef(null);
13
+ const canvas = useRef(null);
14
+ const sparks = useRef([]);
15
+ const raf = useRef(0);
16
+
17
+ useEffect(() => {
18
+ const c = canvas.current;
19
+ const resize = () => {
20
+ const r = wrap.current.getBoundingClientRect();
21
+ const dpr = Math.min(window.devicePixelRatio || 1, 2);
22
+ c.width = r.width * dpr;
23
+ c.height = r.height * dpr;
24
+ c.getContext("2d").setTransform(dpr, 0, 0, dpr, 0, 0);
25
+ };
26
+ const ro = new ResizeObserver(resize);
27
+ ro.observe(wrap.current);
28
+ resize();
29
+ return () => {
30
+ ro.disconnect();
31
+ cancelAnimationFrame(raf.current);
32
+ };
33
+ }, []);
34
+
35
+ const draw = () => {
36
+ const ctx = canvas.current.getContext("2d");
37
+ const now = performance.now();
38
+ ctx.clearRect(0, 0, canvas.current.width, canvas.current.height);
39
+ sparks.current = sparks.current.filter((s) => now - s.start < duration);
40
+ for (const s of sparks.current) {
41
+ const p = ease((now - s.start) / duration);
42
+ const dist = p * sparkRadius;
43
+ const len = sparkSize * (1 - p);
44
+ ctx.strokeStyle = sparkColor;
45
+ ctx.lineWidth = 2;
46
+ ctx.lineCap = "round";
47
+ ctx.beginPath();
48
+ ctx.moveTo(s.x + dist * Math.cos(s.a), s.y + dist * Math.sin(s.a));
49
+ ctx.lineTo(s.x + (dist + len) * Math.cos(s.a), s.y + (dist + len) * Math.sin(s.a));
50
+ ctx.stroke();
51
+ }
52
+ raf.current = sparks.current.length ? requestAnimationFrame(draw) : 0;
53
+ };
54
+
55
+ const onClick = (e) => {
56
+ const r = wrap.current.getBoundingClientRect();
57
+ const x = e.clientX - r.left;
58
+ const y = e.clientY - r.top;
59
+ const start = performance.now();
60
+ for (let i = 0; i < sparkCount; i++) sparks.current.push({ x, y, a: (Math.PI * 2 * i) / sparkCount, start });
61
+ if (!raf.current) raf.current = requestAnimationFrame(draw);
62
+ };
63
+
64
+ return (
65
+ <div ref={wrap} className={`relative ${className}`} onClick={onClick}>
66
+ {children}
67
+ <canvas ref={canvas} aria-hidden="true" className="pointer-events-none absolute inset-0 h-full w-full" />
68
+ </div>
69
+ );
70
+ };
71
+
72
+ export default ClickSpark;
@@ -0,0 +1,108 @@
1
+ import React, { useRef, useState } from "react";
2
+ import { motion } from "framer-motion";
3
+ import { ChevronLeft, ChevronRight } from "lucide-react";
4
+
5
+ /*
6
+ * Coverflow 3D: the classic album-flip view. The centre cover faces you,
7
+ * the rest stand angled on either side and slide back in depth, each with a
8
+ * soft reflection. Drag / swipe, click a cover, or use the arrow keys.
9
+ */
10
+
11
+ const IK = "https://ik.imagekit.io/ny6yinyut/apexui/assets";
12
+ const img = (n) => `${IK}/${n}.png?tr=f-auto,q-80,w-500`;
13
+ const DEFAULT_ITEMS = [
14
+ { image: img("spiderman"), title: "Spider-Man" },
15
+ { image: img("black-panther"), title: "Black Panther" },
16
+ { image: img("hulk"), title: "Hulk" },
17
+ { image: img("captainamerica"), title: "Captain America" },
18
+ { image: img("doctorStrange"), title: "Doctor Strange" },
19
+ { image: img("thanos"), title: "Thanos" },
20
+ { image: img("ironman"), title: "Iron Man" },
21
+ ];
22
+
23
+ const Coverflow3D = ({ items = DEFAULT_ITEMS, initialIndex = 2, size = 12, angle = 55, gap = 0.42, reflection = true, accentColor = "#a3e635", className = "" }) => {
24
+ const [index, setIndex] = useState(initialIndex);
25
+ const drag = useRef(null);
26
+ const clamp = (i) => Math.max(0, Math.min(items.length - 1, i));
27
+ const go = (i) => setIndex(clamp(i));
28
+
29
+ return (
30
+ <div
31
+ className={`flex w-full flex-col items-center gap-6 outline-none ${className}`}
32
+ tabIndex={0}
33
+ onKeyDown={(e) => {
34
+ if (e.key === "ArrowRight") go(index + 1);
35
+ if (e.key === "ArrowLeft") go(index - 1);
36
+ }}
37
+ aria-roledescription="carousel"
38
+ aria-label="Coverflow"
39
+ >
40
+ <div
41
+ className="relative w-full touch-pan-y select-none overflow-hidden [perspective:1100px]"
42
+ style={{ height: `${size * (reflection ? 1.55 : 1.15)}rem` }}
43
+ onPointerDown={(e) => (drag.current = { x: e.clientX, start: index, moved: false })}
44
+ onPointerMove={(e) => {
45
+ const d = drag.current;
46
+ if (!d) return;
47
+ const steps = Math.round((d.x - e.clientX) / 70);
48
+ if (steps !== 0) d.moved = true;
49
+ go(d.start + steps);
50
+ }}
51
+ onPointerUp={() => setTimeout(() => (drag.current = null))}
52
+ onPointerLeave={() => (drag.current = null)}
53
+ >
54
+ {items.map((item, i) => {
55
+ const off = i - index;
56
+ const abs = Math.abs(off);
57
+ const side = Math.sign(off);
58
+ return (
59
+ <motion.button
60
+ key={i}
61
+ type="button"
62
+ aria-label={item.title}
63
+ aria-current={off === 0}
64
+ onClick={() => !drag.current?.moved && go(i)}
65
+ className="absolute left-1/2 top-[6%] [transform-style:preserve-3d]"
66
+ style={{ width: `${size}rem`, height: `${size}rem`, marginLeft: `-${size / 2}rem`, zIndex: 100 - abs }}
67
+ initial={false}
68
+ animate={{
69
+ x: off === 0 ? "0%" : `${side * (55 + (abs - 1) * gap * 100)}%`,
70
+ z: off === 0 ? 60 : -120 - abs * 20,
71
+ rotateY: off === 0 ? 0 : -side * angle,
72
+ opacity: abs > 4 ? 0 : 1,
73
+ }}
74
+ transition={{ type: "spring", stiffness: 220, damping: 30 }}
75
+ >
76
+ <span className="relative block h-full w-full overflow-hidden rounded-xl shadow-[0_25px_50px_-20px_rgba(0,0,0,0.7)] ring-1 ring-white/10">
77
+ <img src={item.image} alt="" draggable="false" className="h-full w-full bg-neutral-900 object-cover" />
78
+ <motion.span className="absolute inset-0 bg-black" initial={false} animate={{ opacity: off === 0 ? 0 : Math.min(0.55, 0.25 + abs * 0.1) }} />
79
+ </span>
80
+ {reflection && (
81
+ <span aria-hidden="true" className="pointer-events-none absolute left-0 top-full mt-1 block h-1/2 w-full overflow-hidden rounded-xl opacity-30 [mask-image:linear-gradient(to_bottom,black,transparent_70%)]">
82
+ <img src={item.image} alt="" draggable="false" className="h-[200%] w-full -scale-y-100 object-cover" />
83
+ </span>
84
+ )}
85
+ </motion.button>
86
+ );
87
+ })}
88
+ </div>
89
+
90
+ <div className="flex items-center gap-4">
91
+ <button type="button" aria-label="Previous" onClick={() => go(index - 1)} disabled={index === 0} className="grid h-10 w-10 place-items-center rounded-full border border-current/20 transition hover:scale-105 active:scale-95 disabled:opacity-30">
92
+ <ChevronLeft className="h-5 w-5" />
93
+ </button>
94
+ <p className="min-w-[9rem] text-center text-sm font-semibold">
95
+ {items[index].title}
96
+ <span className="mt-0.5 block font-mono text-[0.7rem] font-normal opacity-50">
97
+ {index + 1} / {items.length}
98
+ </span>
99
+ </p>
100
+ <button type="button" aria-label="Next" onClick={() => go(index + 1)} disabled={index === items.length - 1} className="grid h-10 w-10 place-items-center rounded-full border border-current/20 transition hover:scale-105 active:scale-95 disabled:opacity-30" style={{ color: index === items.length - 1 ? undefined : accentColor }}>
101
+ <ChevronRight className="h-5 w-5" />
102
+ </button>
103
+ </div>
104
+ </div>
105
+ );
106
+ };
107
+
108
+ export default Coverflow3D;