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.
- package/dist/ApexUI-Kit/templates/AiChat/AiChat.jsx +171 -0
- package/dist/ApexUI-Kit/templates/AiModelPrompt/AiModelPrompt.jsx +221 -0
- package/dist/ApexUI-Kit/templates/AiModelPrompt/index.js +2 -0
- package/dist/ApexUI-Kit/templates/AiModelPrompt/models.js +15 -0
- package/dist/ApexUI-Kit/templates/AiPromptBox/AiPromptBox.jsx +173 -0
- package/dist/ApexUI-Kit/templates/AiResponse/AiResponse.jsx +172 -0
- package/dist/ApexUI-Kit/templates/AiThinking/AiThinking.jsx +106 -0
- package/dist/ApexUI-Kit/templates/AiVoiceBars/AiVoiceBars.jsx +78 -0
- package/dist/ApexUI-Kit/templates/AiVoiceBlob/AiVoiceBlob.jsx +105 -0
- package/dist/ApexUI-Kit/templates/AiVoiceOrb/AiVoiceOrb.jsx +74 -0
- package/dist/ApexUI-Kit/templates/AiVoiceRing/AiVoiceRing.jsx +120 -0
- package/dist/ApexUI-Kit/templates/AiVoiceWave/AiVoiceWave.jsx +107 -0
- package/dist/ApexUI-Kit/templates/AnimatedButton/AnimatedButton.jsx +251 -0
- package/dist/ApexUI-Kit/templates/AnimatedSwitch/AnimatedSwitch.jsx +80 -0
- package/dist/ApexUI-Kit/templates/AnimatedTabs/AnimatedTabs.jsx +107 -0
- package/dist/ApexUI-Kit/templates/AuroraBackground/AuroraBackground.jsx +48 -0
- package/dist/ApexUI-Kit/templates/BlurRevealText/BlurRevealText.jsx +60 -0
- package/dist/ApexUI-Kit/templates/BounceCards/BounceCards.jsx +53 -0
- package/dist/ApexUI-Kit/templates/CardSwap3D/CardSwap3D.jsx +80 -0
- package/dist/ApexUI-Kit/templates/Carousel3D/Carousel3D.jsx +222 -0
- package/dist/ApexUI-Kit/templates/CircularText/CircularText.jsx +49 -0
- package/dist/ApexUI-Kit/templates/ClickSpark/ClickSpark.jsx +72 -0
- package/dist/ApexUI-Kit/templates/Coverflow3D/Coverflow3D.jsx +108 -0
- package/dist/ApexUI-Kit/templates/CubeGallery/CubeGallery.jsx +166 -0
- package/dist/ApexUI-Kit/templates/CursorTrail/CursorTrail.jsx +200 -0
- package/dist/ApexUI-Kit/templates/DotGlobe/DotGlobe.jsx +289 -0
- package/dist/ApexUI-Kit/templates/ElasticSlider/ElasticSlider.jsx +152 -0
- package/dist/ApexUI-Kit/templates/ExtrudedText/ExtrudedText.jsx +78 -0
- package/dist/ApexUI-Kit/templates/FaqAccordion/FaqAccordion.jsx +88 -0
- package/dist/ApexUI-Kit/templates/FlipBook/FlipBook.jsx +191 -0
- package/dist/ApexUI-Kit/templates/FlipClock/FlipClock.jsx +121 -0
- package/dist/ApexUI-Kit/templates/FloatingNavbar/FloatingNavbar.jsx +162 -0
- package/dist/ApexUI-Kit/templates/Folder/Folder.jsx +101 -0
- package/dist/ApexUI-Kit/templates/GlitchText/GlitchText.jsx +56 -0
- package/dist/ApexUI-Kit/templates/GooeyNav/GooeyNav.jsx +310 -0
- package/dist/ApexUI-Kit/templates/HoldToConfirm/HoldToConfirm.jsx +169 -0
- package/dist/ApexUI-Kit/templates/HoldToConfirm/UndoNotice.jsx +58 -0
- package/dist/ApexUI-Kit/templates/HoloCard/HoloCard.jsx +92 -0
- package/dist/ApexUI-Kit/templates/ImageCompare/ImageCompare.jsx +94 -0
- package/dist/ApexUI-Kit/templates/IsoStack/IsoStack.jsx +86 -0
- package/dist/ApexUI-Kit/templates/LiquidGradient/LiquidGradient.jsx +238 -0
- package/dist/ApexUI-Kit/templates/LoadingScreen/LoadingScreen.jsx +805 -0
- package/dist/ApexUI-Kit/templates/MagnetLines/MagnetLines.jsx +67 -0
- package/dist/ApexUI-Kit/templates/MagneticCursor/MagneticCursor.jsx +148 -0
- package/dist/ApexUI-Kit/templates/PixelTransition/PixelTransition.jsx +75 -0
- package/dist/ApexUI-Kit/templates/PricingCards/PricingCards.jsx +115 -0
- package/dist/ApexUI-Kit/templates/ProfileCard/ProfileCard.jsx +213 -0
- package/dist/ApexUI-Kit/templates/ProximityText/ProximityText.jsx +66 -0
- package/dist/ApexUI-Kit/templates/RotatingText/RotatingText.jsx +50 -0
- package/dist/ApexUI-Kit/templates/ShinyText/ShinyText.jsx +30 -0
- package/dist/ApexUI-Kit/templates/SidebarNav/SidebarNav.jsx +220 -0
- package/dist/ApexUI-Kit/templates/SpotlightGrid/SpotlightGrid.jsx +110 -0
- package/dist/ApexUI-Kit/templates/StarBorder/StarBorder.jsx +33 -0
- package/dist/ApexUI-Kit/templates/Stepper/Stepper.jsx +129 -0
- package/dist/ApexUI-Kit/templates/SwipeCards/SwipeCards.jsx +123 -0
- package/dist/ApexUI-Kit/templates/TextRing3D/TextRing3D.jsx +73 -0
- package/dist/ApexUI-Kit/templates/ToastStack/ToastStack.jsx +108 -0
- package/dist/ApexUI-Kit/templates/ToolTip/ToolTip.jsx +5 -3
- package/dist/ApexUI-Kit/templates/TrueFocus/TrueFocus.jsx +64 -0
- package/dist/ApexUI-Kit/templates/TypewriterText/TypewriterText.jsx +70 -0
- package/dist/ApexUI-Kit/templates/WebGLCarousel/WebGLCarousel.jsx +326 -0
- package/dist/ApexUI-Kit/templates/WorldMap/WorldMap.jsx +359 -0
- package/dist/cli/index.js +63 -0
- package/package.json +1 -1
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { AnimatePresence, motion } from "framer-motion";
|
|
3
|
+
import { Mic, MicOff } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* AI Voice Wave: layered glowing sine waves, like a phone assistant. Flat
|
|
7
|
+
* and calm when idle, lively while listening, a slow swell while thinking
|
|
8
|
+
* and full, syllable-like bursts while speaking. Tap to cycle the states or
|
|
9
|
+
* control `state` yourself.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const LABELS = { idle: "Tap to talk", listening: "Listening…", thinking: "Thinking…", speaking: "Speaking…" };
|
|
13
|
+
const ORDER = ["idle", "listening", "thinking", "speaking"];
|
|
14
|
+
|
|
15
|
+
// How loud the voice "is" at time t for each state (0–1).
|
|
16
|
+
const levelAt = (state, t) =>
|
|
17
|
+
state === "speaking" ? 0.45 + 0.55 * Math.abs(Math.sin(t * 8.5) * Math.sin(t * 2.1)) : state === "listening" ? 0.35 + 0.45 * Math.abs(Math.sin(t * 5.2) * Math.cos(t * 1.6)) : state === "thinking" ? 0.22 + 0.1 * Math.sin(t * 2) : 0.06;
|
|
18
|
+
|
|
19
|
+
const AiVoiceWave = ({ state: stateProp, onStateChange, colors = ["#a3e635", "#22d3ee", "#a78bfa"], height = 9, showLabel = true, className = "" }) => {
|
|
20
|
+
const canvasRef = useRef(null);
|
|
21
|
+
const [inner, setInner] = useState("idle");
|
|
22
|
+
const state = stateProp ?? inner;
|
|
23
|
+
const stateRef = useRef(state);
|
|
24
|
+
stateRef.current = state;
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const canvas = canvasRef.current;
|
|
28
|
+
const ctx = canvas.getContext("2d");
|
|
29
|
+
let raf = 0;
|
|
30
|
+
let level = 0.05;
|
|
31
|
+
const resize = () => {
|
|
32
|
+
const r = canvas.getBoundingClientRect();
|
|
33
|
+
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
34
|
+
canvas.width = Math.round(r.width * dpr);
|
|
35
|
+
canvas.height = Math.round(r.height * dpr);
|
|
36
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
37
|
+
};
|
|
38
|
+
const ro = new ResizeObserver(resize);
|
|
39
|
+
ro.observe(canvas);
|
|
40
|
+
resize();
|
|
41
|
+
|
|
42
|
+
const draw = (now) => {
|
|
43
|
+
const t = now / 1000;
|
|
44
|
+
const { width: w, height: h } = canvas.getBoundingClientRect();
|
|
45
|
+
level += (levelAt(stateRef.current, t) - level) * 0.12;
|
|
46
|
+
ctx.clearRect(0, 0, w, h);
|
|
47
|
+
colors.forEach((c, k) => {
|
|
48
|
+
const amp = h * 0.42 * level * (1 - k * 0.18);
|
|
49
|
+
const freq = 1.6 + k * 0.7;
|
|
50
|
+
const phase = t * (2.2 + k * 0.9) * (stateRef.current === "thinking" ? 0.5 : 1);
|
|
51
|
+
ctx.beginPath();
|
|
52
|
+
for (let x = 0; x <= w; x += 2) {
|
|
53
|
+
const u = x / w;
|
|
54
|
+
// Taper both ends so the wave fades in from a flat line.
|
|
55
|
+
const taper = Math.pow(Math.sin(Math.PI * u), 2.2);
|
|
56
|
+
const y = h / 2 + Math.sin(u * Math.PI * 2 * freq + phase) * amp * taper;
|
|
57
|
+
if (x === 0) ctx.moveTo(x, y);
|
|
58
|
+
else ctx.lineTo(x, y);
|
|
59
|
+
}
|
|
60
|
+
ctx.strokeStyle = c;
|
|
61
|
+
ctx.lineWidth = 3 - k * 0.5;
|
|
62
|
+
ctx.shadowColor = c;
|
|
63
|
+
ctx.shadowBlur = 14;
|
|
64
|
+
ctx.globalAlpha = 0.85;
|
|
65
|
+
ctx.stroke();
|
|
66
|
+
});
|
|
67
|
+
ctx.globalAlpha = 1;
|
|
68
|
+
ctx.shadowBlur = 0;
|
|
69
|
+
raf = requestAnimationFrame(draw);
|
|
70
|
+
};
|
|
71
|
+
raf = requestAnimationFrame(draw);
|
|
72
|
+
return () => {
|
|
73
|
+
cancelAnimationFrame(raf);
|
|
74
|
+
ro.disconnect();
|
|
75
|
+
};
|
|
76
|
+
}, [colors]);
|
|
77
|
+
|
|
78
|
+
const next = () => {
|
|
79
|
+
const n = ORDER[(ORDER.indexOf(state) + 1) % ORDER.length];
|
|
80
|
+
if (stateProp === undefined) setInner(n);
|
|
81
|
+
onStateChange?.(n);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div className={`flex w-full max-w-xl flex-col items-center gap-4 ${className}`}>
|
|
86
|
+
<button type="button" onClick={next} aria-label={`Voice assistant: ${LABELS[state]}`} className="w-full rounded-3xl outline-none focus-visible:ring-2 focus-visible:ring-white/30">
|
|
87
|
+
<canvas ref={canvasRef} aria-hidden="true" className="block w-full" style={{ height: `${height}rem` }} />
|
|
88
|
+
</button>
|
|
89
|
+
{showLabel && <Label state={state} color={colors[0]} />}
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
function Label({ state, color }) {
|
|
95
|
+
return (
|
|
96
|
+
<div className="flex items-center gap-2 text-sm font-medium">
|
|
97
|
+
{state === "idle" ? <MicOff className="h-4 w-4 opacity-60" /> : <Mic className="h-4 w-4" style={{ color }} />}
|
|
98
|
+
<AnimatePresence mode="wait">
|
|
99
|
+
<motion.span key={state} initial={{ opacity: 0, y: 6 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -6 }} transition={{ duration: 0.18 }}>
|
|
100
|
+
{LABELS[state]}
|
|
101
|
+
</motion.span>
|
|
102
|
+
</AnimatePresence>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export default AiVoiceWave;
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import React, { useRef, useState } from "react";
|
|
2
|
+
import { AnimatePresence, motion } from "framer-motion";
|
|
3
|
+
import { ArrowRight, Check, Loader2 } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* Animated Button: one button, seven feels. Pick a variant:
|
|
7
|
+
* shine – a gleam sweeps across on hover
|
|
8
|
+
* fill – color floods in from where the pointer entered and drains out where it left
|
|
9
|
+
* border – a light beam runs around the border
|
|
10
|
+
* press – a chunky 3D key that sinks when pressed
|
|
11
|
+
* loading – spins while onClick's promise runs, then shows a check
|
|
12
|
+
* ripple – a ripple spreads from every click
|
|
13
|
+
* arrow – the arrow slides through and the label nudges over
|
|
14
|
+
* Every motion is a GPU transform with one shared easing, so it stays smooth.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const EASE = "cubic-bezier(0.22, 1, 0.36, 1)";
|
|
18
|
+
|
|
19
|
+
const SIZES = {
|
|
20
|
+
sm: { box: "h-9 px-4 text-[0.8rem] gap-1.5", pad: "px-4 gap-1.5" },
|
|
21
|
+
md: { box: "h-11 px-5 text-[0.9rem] gap-2", pad: "px-5 gap-2" },
|
|
22
|
+
lg: { box: "h-[3.25rem] px-7 text-base gap-2.5", pad: "px-7 gap-2.5" },
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Black or white text, whichever reads better on the accent.
|
|
26
|
+
const inkOn = (hex) => {
|
|
27
|
+
let h = String(hex).replace("#", "");
|
|
28
|
+
if (h.length === 3) h = h.split("").map((c) => c + c).join("");
|
|
29
|
+
const n = parseInt(h, 16) || 0;
|
|
30
|
+
const l = (0.299 * ((n >> 16) & 255) + 0.587 * ((n >> 8) & 255) + 0.114 * (n & 255)) / 255;
|
|
31
|
+
return l > 0.6 ? "#0a0a0a" : "#ffffff";
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const AnimatedButton = ({ variant = "shine", children = "Get started", color = "#a3e635", size = "md", onClick, disabled, className = "", ...rest }) => {
|
|
35
|
+
const ref = useRef(null);
|
|
36
|
+
const [fill, setFill] = useState({ x: 0, y: 0, d: 0, on: false });
|
|
37
|
+
const [ripples, setRipples] = useState([]);
|
|
38
|
+
const [status, setStatus] = useState("idle");
|
|
39
|
+
const ink = inkOn(color);
|
|
40
|
+
const s = SIZES[size] ?? SIZES.md;
|
|
41
|
+
const base = `relative inline-flex select-none items-center justify-center overflow-hidden rounded-full font-semibold outline-none [-webkit-tap-highlight-color:transparent] focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 ${s.box}`;
|
|
42
|
+
const smooth = { transitionTimingFunction: EASE };
|
|
43
|
+
|
|
44
|
+
const localPoint = (e) => {
|
|
45
|
+
const r = ref.current.getBoundingClientRect();
|
|
46
|
+
return { x: e.clientX - r.left, y: e.clientY - r.top, w: r.width, h: r.height };
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
if (variant === "shine") {
|
|
50
|
+
return (
|
|
51
|
+
<button
|
|
52
|
+
ref={ref}
|
|
53
|
+
type="button"
|
|
54
|
+
onClick={onClick}
|
|
55
|
+
disabled={disabled}
|
|
56
|
+
className={`group ${base} bg-neutral-950 text-white transition-[transform,box-shadow] duration-300 hover:-translate-y-0.5 hover:shadow-[0_12px_32px_-12px_var(--c)] active:translate-y-0 active:scale-[0.97] active:duration-100 ${className}`}
|
|
57
|
+
style={{ "--c": color, ...smooth }}
|
|
58
|
+
{...rest}
|
|
59
|
+
>
|
|
60
|
+
<span aria-hidden="true" className="absolute inset-0 rounded-full transition-opacity duration-300 group-hover:opacity-100" style={{ boxShadow: `inset 0 0 0 1px ${color}66, inset 0 1px 0 rgba(255,255,255,0.12)` }} />
|
|
61
|
+
<span
|
|
62
|
+
aria-hidden="true"
|
|
63
|
+
className="absolute inset-y-0 left-0 w-1/2 -translate-x-[120%] skew-x-[-20deg] bg-gradient-to-r from-transparent via-white/35 to-transparent transition-transform duration-[900ms] will-change-transform group-hover:translate-x-[260%]"
|
|
64
|
+
style={smooth}
|
|
65
|
+
/>
|
|
66
|
+
<span className="relative">{children}</span>
|
|
67
|
+
</button>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (variant === "fill") {
|
|
72
|
+
// One circle big enough to cover the button, scaled 0 → 1 from the entry point.
|
|
73
|
+
const track = (e, on) => {
|
|
74
|
+
const p = localPoint(e);
|
|
75
|
+
setFill({ x: p.x, y: p.y, d: Math.hypot(Math.max(p.x, p.w - p.x), Math.max(p.y, p.h - p.y)) * 2, on });
|
|
76
|
+
};
|
|
77
|
+
return (
|
|
78
|
+
<button
|
|
79
|
+
ref={ref}
|
|
80
|
+
type="button"
|
|
81
|
+
onClick={onClick}
|
|
82
|
+
disabled={disabled}
|
|
83
|
+
onPointerEnter={(e) => track(e, true)}
|
|
84
|
+
onPointerLeave={(e) => track(e, false)}
|
|
85
|
+
className={`${base} border-[1.5px] transition-[color,transform] duration-500 active:scale-[0.97] active:duration-100 ${className}`}
|
|
86
|
+
style={{ borderColor: color, color: fill.on ? ink : color, ...smooth }}
|
|
87
|
+
{...rest}
|
|
88
|
+
>
|
|
89
|
+
<span
|
|
90
|
+
aria-hidden="true"
|
|
91
|
+
className="pointer-events-none absolute rounded-full will-change-transform"
|
|
92
|
+
style={{
|
|
93
|
+
left: fill.x - fill.d / 2,
|
|
94
|
+
top: fill.y - fill.d / 2,
|
|
95
|
+
width: fill.d,
|
|
96
|
+
height: fill.d,
|
|
97
|
+
background: color,
|
|
98
|
+
transform: `scale(${fill.on ? 1 : 0})`,
|
|
99
|
+
transition: `transform 550ms ${EASE}`,
|
|
100
|
+
}}
|
|
101
|
+
/>
|
|
102
|
+
<span className="relative">{children}</span>
|
|
103
|
+
</button>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (variant === "border") {
|
|
108
|
+
return (
|
|
109
|
+
<button
|
|
110
|
+
ref={ref}
|
|
111
|
+
type="button"
|
|
112
|
+
onClick={onClick}
|
|
113
|
+
disabled={disabled}
|
|
114
|
+
className={`group ${base} text-white transition-[transform,box-shadow] duration-300 hover:shadow-[0_0_30px_-8px_var(--c)] active:scale-[0.97] active:duration-100 ${className}`}
|
|
115
|
+
style={{ "--c": color, padding: "1.5px", ...smooth }}
|
|
116
|
+
{...rest}
|
|
117
|
+
>
|
|
118
|
+
<motion.span
|
|
119
|
+
aria-hidden="true"
|
|
120
|
+
className="absolute left-1/2 top-1/2 aspect-square w-[260%] -translate-x-1/2 -translate-y-1/2 will-change-transform"
|
|
121
|
+
style={{ background: `conic-gradient(from 0deg, transparent 0 68%, ${color}66 78%, ${color} 88%, transparent 100%)` }}
|
|
122
|
+
animate={{ rotate: 360 }}
|
|
123
|
+
transition={{ duration: 2.8, repeat: Infinity, ease: "linear" }}
|
|
124
|
+
/>
|
|
125
|
+
<span className={`relative flex h-full w-full items-center justify-center rounded-full bg-neutral-950 transition-colors duration-300 group-hover:bg-neutral-900 ${s.pad}`}>{children}</span>
|
|
126
|
+
</button>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (variant === "press") {
|
|
131
|
+
const edge = `color-mix(in srgb, ${color} 55%, #000)`;
|
|
132
|
+
return (
|
|
133
|
+
<button
|
|
134
|
+
ref={ref}
|
|
135
|
+
type="button"
|
|
136
|
+
onClick={onClick}
|
|
137
|
+
disabled={disabled}
|
|
138
|
+
className={`${base} -translate-y-1 shadow-[0_5px_0_var(--edge),0_14px_22px_-10px_rgba(0,0,0,0.55)] transition-[transform,box-shadow] duration-200 hover:-translate-y-[6px] hover:shadow-[0_7px_0_var(--edge),0_18px_26px_-12px_rgba(0,0,0,0.55)] active:translate-y-0 active:shadow-[0_1px_0_var(--edge),0_4px_8px_-4px_rgba(0,0,0,0.4)] active:duration-75 ${className}`}
|
|
139
|
+
style={{ background: color, color: ink, "--edge": edge, ...smooth }}
|
|
140
|
+
{...rest}
|
|
141
|
+
>
|
|
142
|
+
<span aria-hidden="true" className="absolute inset-x-3 top-[3px] h-1/3 rounded-full bg-white/25 blur-[2px]" />
|
|
143
|
+
<span className="relative">{children}</span>
|
|
144
|
+
</button>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (variant === "loading") {
|
|
149
|
+
const run = async (e) => {
|
|
150
|
+
if (status !== "idle") return;
|
|
151
|
+
setStatus("loading");
|
|
152
|
+
try {
|
|
153
|
+
const result = onClick?.(e);
|
|
154
|
+
await (result && typeof result.then === "function" ? result : new Promise((r) => setTimeout(r, 1400)));
|
|
155
|
+
setStatus("done");
|
|
156
|
+
} catch {
|
|
157
|
+
setStatus("idle");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
setTimeout(() => setStatus("idle"), 1600);
|
|
161
|
+
};
|
|
162
|
+
return (
|
|
163
|
+
<motion.button
|
|
164
|
+
ref={ref}
|
|
165
|
+
type="button"
|
|
166
|
+
layout
|
|
167
|
+
onClick={run}
|
|
168
|
+
disabled={disabled}
|
|
169
|
+
aria-busy={status === "loading"}
|
|
170
|
+
className={`${base} transition-[background-color,color] duration-300 active:scale-[0.97] ${className}`}
|
|
171
|
+
style={{ background: status === "done" ? "#22c55e" : color, color: status === "done" ? "#fff" : ink, borderRadius: 999, ...smooth }}
|
|
172
|
+
transition={{ layout: { type: "spring", stiffness: 420, damping: 34 } }}
|
|
173
|
+
{...rest}
|
|
174
|
+
>
|
|
175
|
+
<AnimatePresence mode="popLayout" initial={false}>
|
|
176
|
+
<motion.span
|
|
177
|
+
key={status}
|
|
178
|
+
className="flex items-center gap-2"
|
|
179
|
+
initial={{ opacity: 0, y: 12, scale: 0.9 }}
|
|
180
|
+
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
181
|
+
exit={{ opacity: 0, y: -12, scale: 0.9 }}
|
|
182
|
+
transition={{ duration: 0.25, ease: [0.22, 1, 0.36, 1] }}
|
|
183
|
+
>
|
|
184
|
+
{status === "idle" && children}
|
|
185
|
+
{status === "loading" && <Loader2 className="h-[1.15em] w-[1.15em] animate-spin" aria-label="Loading" />}
|
|
186
|
+
{status === "done" && (
|
|
187
|
+
<>
|
|
188
|
+
<Check className="h-[1.15em] w-[1.15em]" /> Done
|
|
189
|
+
</>
|
|
190
|
+
)}
|
|
191
|
+
</motion.span>
|
|
192
|
+
</AnimatePresence>
|
|
193
|
+
</motion.button>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (variant === "ripple") {
|
|
198
|
+
return (
|
|
199
|
+
<button
|
|
200
|
+
ref={ref}
|
|
201
|
+
type="button"
|
|
202
|
+
disabled={disabled}
|
|
203
|
+
onPointerDown={(e) => {
|
|
204
|
+
const p = localPoint(e);
|
|
205
|
+
setRipples((list) => [...list, { id: performance.now(), x: p.x, y: p.y, d: Math.hypot(p.w, p.h) * 2.2 }]);
|
|
206
|
+
}}
|
|
207
|
+
onClick={onClick}
|
|
208
|
+
className={`${base} transition-[transform,filter] duration-300 hover:brightness-110 active:scale-[0.98] active:duration-100 ${className}`}
|
|
209
|
+
style={{ background: color, color: ink, ...smooth }}
|
|
210
|
+
{...rest}
|
|
211
|
+
>
|
|
212
|
+
{ripples.map((r) => (
|
|
213
|
+
<motion.span
|
|
214
|
+
key={r.id}
|
|
215
|
+
aria-hidden="true"
|
|
216
|
+
className="pointer-events-none absolute rounded-full will-change-transform"
|
|
217
|
+
style={{ left: r.x - r.d / 2, top: r.y - r.d / 2, width: r.d, height: r.d, background: ink }}
|
|
218
|
+
initial={{ scale: 0, opacity: 0.28 }}
|
|
219
|
+
animate={{ scale: 1, opacity: 0 }}
|
|
220
|
+
transition={{ duration: 0.75, ease: [0.22, 1, 0.36, 1] }}
|
|
221
|
+
onAnimationComplete={() => setRipples((list) => list.filter((x) => x.id !== r.id))}
|
|
222
|
+
/>
|
|
223
|
+
))}
|
|
224
|
+
<span className="relative">{children}</span>
|
|
225
|
+
</button>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// arrow
|
|
230
|
+
return (
|
|
231
|
+
<button
|
|
232
|
+
ref={ref}
|
|
233
|
+
type="button"
|
|
234
|
+
onClick={onClick}
|
|
235
|
+
disabled={disabled}
|
|
236
|
+
className={`group ${base} pr-1.5 transition-transform duration-300 active:scale-[0.97] active:duration-100 ${className}`}
|
|
237
|
+
style={{ background: color, color: ink, ...smooth }}
|
|
238
|
+
{...rest}
|
|
239
|
+
>
|
|
240
|
+
<span className="relative transition-transform duration-500 group-hover:-translate-x-1" style={smooth}>
|
|
241
|
+
{children}
|
|
242
|
+
</span>
|
|
243
|
+
<span className="relative grid aspect-square h-[calc(100%-0.5rem)] place-items-center overflow-hidden rounded-full transition-transform duration-500 group-hover:scale-105" style={{ background: ink, color, ...smooth }}>
|
|
244
|
+
<ArrowRight className="absolute h-[1.1em] w-[1.1em] transition-transform duration-500 group-hover:translate-x-[180%]" style={smooth} />
|
|
245
|
+
<ArrowRight className="absolute h-[1.1em] w-[1.1em] -translate-x-[180%] transition-transform duration-500 group-hover:translate-x-0" style={smooth} />
|
|
246
|
+
</span>
|
|
247
|
+
</button>
|
|
248
|
+
);
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
export default AnimatedButton;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import React, { useId, useState } from "react";
|
|
2
|
+
import { motion } from "framer-motion";
|
|
3
|
+
import { Check, Moon, Sun, X } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* Animated Switch: a toggle with personality. Variants:
|
|
7
|
+
* classic – the familiar pill with a springy knob that stretches as it moves
|
|
8
|
+
* icon – the knob shows a cross or a check
|
|
9
|
+
* daynight – a sun crosses into a starry night
|
|
10
|
+
* neon – a glowing track that lights up when on
|
|
11
|
+
* Works controlled (`checked` + `onChange`) or on its own (`defaultChecked`).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const knobSpring = { type: "spring", stiffness: 520, damping: 32 };
|
|
15
|
+
|
|
16
|
+
const AnimatedSwitch = ({ variant = "classic", checked, defaultChecked = false, onChange, label, accentColor = "#a3e635", size = 1, disabled = false, className = "" }) => {
|
|
17
|
+
const id = useId();
|
|
18
|
+
const [inner, setInner] = useState(defaultChecked);
|
|
19
|
+
const on = checked ?? inner;
|
|
20
|
+
const toggle = () => {
|
|
21
|
+
if (disabled) return;
|
|
22
|
+
if (checked === undefined) setInner(!on);
|
|
23
|
+
onChange?.(!on);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const W = 3.25 * size;
|
|
27
|
+
const H = 1.85 * size;
|
|
28
|
+
const K = H - 0.35 * size;
|
|
29
|
+
const travel = W - K - 0.35 * size;
|
|
30
|
+
|
|
31
|
+
const track = {
|
|
32
|
+
classic: { background: on ? accentColor : "rgba(120,120,128,0.32)" },
|
|
33
|
+
icon: { background: on ? accentColor : "rgba(120,120,128,0.32)" },
|
|
34
|
+
daynight: { background: on ? "linear-gradient(135deg,#0f172a,#312e81)" : "linear-gradient(135deg,#7dd3fc,#38bdf8)" },
|
|
35
|
+
neon: { background: "#0a0a0a", boxShadow: on ? `0 0 0 1.5px ${accentColor}, 0 0 18px ${accentColor}99, inset 0 0 12px ${accentColor}55` : "0 0 0 1.5px rgba(255,255,255,0.15)" },
|
|
36
|
+
}[variant];
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<label htmlFor={id} className={`inline-flex select-none items-center gap-3 ${disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer"} ${className}`}>
|
|
40
|
+
<button
|
|
41
|
+
id={id}
|
|
42
|
+
type="button"
|
|
43
|
+
role="switch"
|
|
44
|
+
aria-checked={on}
|
|
45
|
+
disabled={disabled}
|
|
46
|
+
onClick={toggle}
|
|
47
|
+
className="relative shrink-0 overflow-hidden rounded-full outline-none transition-[background,box-shadow] duration-300 focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
48
|
+
style={{ width: `${W}rem`, height: `${H}rem`, ...track }}
|
|
49
|
+
>
|
|
50
|
+
{variant === "daynight" && (
|
|
51
|
+
<>
|
|
52
|
+
{[0.9, 1.6, 2.2, 1.2].map((x, i) => (
|
|
53
|
+
<motion.span
|
|
54
|
+
key={i}
|
|
55
|
+
className="absolute h-[0.14rem] w-[0.14rem] rounded-full bg-white"
|
|
56
|
+
style={{ left: `${x * size * 0.7}rem`, top: `${(0.35 + (i % 2) * 0.55) * size}rem` }}
|
|
57
|
+
animate={{ opacity: on ? [0.3, 1, 0.3] : 0, scale: on ? 1 : 0 }}
|
|
58
|
+
transition={{ duration: 1.6, repeat: on ? Infinity : 0, delay: i * 0.3 }}
|
|
59
|
+
/>
|
|
60
|
+
))}
|
|
61
|
+
<motion.span className="absolute rounded-full bg-white/80" style={{ width: `${0.9 * size}rem`, height: `${0.35 * size}rem`, top: `${1.05 * size}rem` }} animate={{ left: on ? `${-1.2 * size}rem` : `${1.7 * size}rem`, opacity: on ? 0 : 1 }} transition={{ duration: 0.4 }} />
|
|
62
|
+
</>
|
|
63
|
+
)}
|
|
64
|
+
<motion.span
|
|
65
|
+
className="absolute grid place-items-center rounded-full shadow-[0_2px_6px_rgba(0,0,0,0.3)]"
|
|
66
|
+
style={{ left: `${0.175 * size}rem`, top: `${(H - K) / 2}rem`, height: `${K}rem`, background: variant === "daynight" ? (on ? "#e2e8f0" : "#fde047") : variant === "neon" ? (on ? accentColor : "#3f3f46") : "#ffffff" }}
|
|
67
|
+
initial={false}
|
|
68
|
+
animate={{ x: on ? `${travel}rem` : "0rem", width: on ? [`${K}rem`, `${K * 1.25}rem`, `${K}rem`] : [`${K}rem`, `${K * 1.24}rem`, `${K}rem`] }}
|
|
69
|
+
transition={{ x: knobSpring, width: { duration: 0.3 } }}
|
|
70
|
+
>
|
|
71
|
+
{variant === "icon" && (on ? <Check className="h-3/5 w-3/5" style={{ color: accentColor }} strokeWidth={3} /> : <X className="h-3/5 w-3/5 text-neutral-400" strokeWidth={3} />)}
|
|
72
|
+
{variant === "daynight" && (on ? <Moon className="h-3/5 w-3/5 text-slate-500" fill="currentColor" /> : <Sun className="h-3/5 w-3/5 text-amber-600" />)}
|
|
73
|
+
</motion.span>
|
|
74
|
+
</button>
|
|
75
|
+
{label && <span className="text-sm font-medium">{label}</span>}
|
|
76
|
+
</label>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export default AnimatedSwitch;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import React, { useId, useRef, useState } from "react";
|
|
2
|
+
import { AnimatePresence, motion } from "framer-motion";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Animated Tabs: a pill slides to the chosen tab and the panel below
|
|
6
|
+
* cross-fades in the direction you moved. Arrow keys, Home and End work.
|
|
7
|
+
* Styles: "pill" (filled), "underline" or "segmented".
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const DEFAULT_TABS = [
|
|
11
|
+
{ id: "overview", label: "Overview", content: "A quick look at everything in one place — usage, limits and the latest activity." },
|
|
12
|
+
{ id: "analytics", label: "Analytics", content: "Visitors, conversions and retention, updated every few seconds with zero setup." },
|
|
13
|
+
{ id: "reports", label: "Reports", content: "Export weekly PDF reports or schedule them to land in your team's inbox." },
|
|
14
|
+
{ id: "settings", label: "Settings", content: "Workspace name, members, billing and the danger zone — all in one tidy screen." },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const AnimatedTabs = ({ tabs = DEFAULT_TABS, defaultTab, variant = "pill", accentColor = "#a3e635", theme = "dark", onChange, className = "" }) => {
|
|
18
|
+
const id = useId();
|
|
19
|
+
const [active, setActive] = useState(defaultTab ?? tabs[0]?.id);
|
|
20
|
+
const [dir, setDir] = useState(1);
|
|
21
|
+
const refs = useRef([]);
|
|
22
|
+
const dark = theme !== "light";
|
|
23
|
+
const index = tabs.findIndex((t) => t.id === active);
|
|
24
|
+
const current = tabs[index];
|
|
25
|
+
|
|
26
|
+
const select = (i) => {
|
|
27
|
+
const t = tabs[(i + tabs.length) % tabs.length];
|
|
28
|
+
setDir(tabs.indexOf(t) > index ? 1 : -1);
|
|
29
|
+
setActive(t.id);
|
|
30
|
+
refs.current[tabs.indexOf(t)]?.focus();
|
|
31
|
+
onChange?.(t.id);
|
|
32
|
+
};
|
|
33
|
+
const onKeyDown = (e) => {
|
|
34
|
+
const map = { ArrowRight: index + 1, ArrowLeft: index - 1, Home: 0, End: tabs.length - 1 };
|
|
35
|
+
if (map[e.key] === undefined) return;
|
|
36
|
+
e.preventDefault();
|
|
37
|
+
select(map[e.key]);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const list = {
|
|
41
|
+
pill: `rounded-full p-1 ${dark ? "bg-white/[0.06]" : "bg-black/[0.05]"}`,
|
|
42
|
+
segmented: `rounded-xl p-1 ${dark ? "bg-white/[0.06] ring-1 ring-white/10" : "bg-black/[0.04] ring-1 ring-black/10"}`,
|
|
43
|
+
underline: `border-b ${dark ? "border-white/10" : "border-black/10"}`,
|
|
44
|
+
}[variant];
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className={`w-full max-w-xl ${dark ? "text-white" : "text-neutral-900"} ${className}`}>
|
|
48
|
+
<div role="tablist" aria-orientation="horizontal" onKeyDown={onKeyDown} className={`relative flex overflow-x-auto [scrollbar-width:none] ${list}`}>
|
|
49
|
+
{tabs.map((t, i) => {
|
|
50
|
+
const on = t.id === active;
|
|
51
|
+
return (
|
|
52
|
+
<button
|
|
53
|
+
key={t.id}
|
|
54
|
+
ref={(el) => (refs.current[i] = el)}
|
|
55
|
+
role="tab"
|
|
56
|
+
id={`${id}-tab-${t.id}`}
|
|
57
|
+
aria-selected={on}
|
|
58
|
+
aria-controls={`${id}-panel`}
|
|
59
|
+
tabIndex={on ? 0 : -1}
|
|
60
|
+
onClick={() => select(i)}
|
|
61
|
+
className={`relative flex-1 whitespace-nowrap px-4 py-2 text-[0.88rem] font-medium outline-none transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-current/40 ${variant === "underline" ? "pb-3" : variant === "pill" ? "rounded-full" : "rounded-lg"} ${on ? (variant === "pill" ? "text-neutral-950" : "") : dark ? "text-white/55 hover:text-white" : "text-neutral-500 hover:text-neutral-900"}`}
|
|
62
|
+
>
|
|
63
|
+
{on && (
|
|
64
|
+
<motion.span
|
|
65
|
+
layoutId={`${id}-indicator`}
|
|
66
|
+
className={`absolute ${variant === "underline" ? "inset-x-2 -bottom-px h-[2px] rounded-full" : `inset-0 ${variant === "pill" ? "rounded-full" : "rounded-lg"}`}`}
|
|
67
|
+
style={{
|
|
68
|
+
background: variant === "segmented" ? (dark ? "rgba(255,255,255,0.1)" : "#fff") : accentColor,
|
|
69
|
+
boxShadow: variant === "segmented" ? (dark ? "inset 0 1px 0 rgba(255,255,255,0.08)" : "0 1px 3px rgba(0,0,0,0.12)") : undefined,
|
|
70
|
+
}}
|
|
71
|
+
transition={{ type: "spring", stiffness: 480, damping: 36 }}
|
|
72
|
+
/>
|
|
73
|
+
)}
|
|
74
|
+
<span className="relative">{t.label}</span>
|
|
75
|
+
</button>
|
|
76
|
+
);
|
|
77
|
+
})}
|
|
78
|
+
</div>
|
|
79
|
+
<div id={`${id}-panel`} role="tabpanel" aria-labelledby={`${id}-tab-${active}`} className="relative mt-4 grid min-h-[6rem] overflow-hidden">
|
|
80
|
+
{/* Invisible copies of every panel reserve the tallest height, so the
|
|
81
|
+
tab bar never jumps when panels of different lengths swap. */}
|
|
82
|
+
{tabs.map((t) => (
|
|
83
|
+
<div key={t.id} aria-hidden="true" className="invisible col-start-1 row-start-1 p-5 text-[0.92rem] leading-relaxed">
|
|
84
|
+
<p className="mb-1.5 text-[1rem] font-semibold">{t.label}</p>
|
|
85
|
+
{t.content}
|
|
86
|
+
</div>
|
|
87
|
+
))}
|
|
88
|
+
<AnimatePresence mode="popLayout" initial={false} custom={dir}>
|
|
89
|
+
<motion.div
|
|
90
|
+
key={active}
|
|
91
|
+
custom={dir}
|
|
92
|
+
initial={{ opacity: 0, x: dir * 24, filter: "blur(4px)" }}
|
|
93
|
+
animate={{ opacity: 1, x: 0, filter: "blur(0px)" }}
|
|
94
|
+
exit={{ opacity: 0, x: dir * -24, filter: "blur(4px)" }}
|
|
95
|
+
transition={{ duration: 0.28, ease: [0.22, 1, 0.36, 1] }}
|
|
96
|
+
className={`col-start-1 row-start-1 h-full rounded-2xl p-5 text-[0.92rem] leading-relaxed ${dark ? "bg-white/[0.04] text-white/75" : "bg-black/[0.03] text-neutral-600"}`}
|
|
97
|
+
>
|
|
98
|
+
<p className={`mb-1.5 text-[1rem] font-semibold ${dark ? "text-white" : "text-neutral-900"}`}>{current?.label}</p>
|
|
99
|
+
{current?.content}
|
|
100
|
+
</motion.div>
|
|
101
|
+
</AnimatePresence>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export default AnimatedTabs;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Aurora Background: soft northern-light bands that drift and shimmer, built
|
|
5
|
+
* from blurred CSS gradients only (no canvas, no WebGL). Works on light and
|
|
6
|
+
* dark pages; reduced-motion visitors get the same picture without movement.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const AuroraBackground = ({
|
|
10
|
+
children,
|
|
11
|
+
colors = ["#22d3ee", "#a3e635", "#a78bfa", "#38bdf8"],
|
|
12
|
+
theme = "dark",
|
|
13
|
+
speed = 1,
|
|
14
|
+
intensity = 1,
|
|
15
|
+
className = "",
|
|
16
|
+
containerClassName = "",
|
|
17
|
+
}) => {
|
|
18
|
+
const dark = theme !== "light";
|
|
19
|
+
const stops = colors.map((c, i) => `${c} ${Math.round((i / colors.length) * 100)}%`).join(", ");
|
|
20
|
+
const duration = `${60 / Math.max(0.1, speed)}s`;
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className={`relative h-screen w-full overflow-hidden ${dark ? "bg-[#05060a]" : "bg-[#f8fafc]"} ${containerClassName}`}>
|
|
24
|
+
<style>{`
|
|
25
|
+
@keyframes apex-aurora { from { background-position: 50% 50%, 50% 50%; } to { background-position: 350% 50%, 350% 50%; } }
|
|
26
|
+
@media (prefers-reduced-motion: reduce) { .apex-aurora, .apex-aurora::after { animation: none !important; } }
|
|
27
|
+
`}</style>
|
|
28
|
+
<div
|
|
29
|
+
aria-hidden="true"
|
|
30
|
+
className="apex-aurora pointer-events-none absolute -inset-[10px] will-change-transform after:absolute after:inset-0 after:content-['']"
|
|
31
|
+
style={{
|
|
32
|
+
opacity: Math.min(1, (dark ? 0.9 : 0.75) * intensity),
|
|
33
|
+
filter: `blur(${dark ? 12 : 10}px) ${dark ? "" : "invert(0)"}`,
|
|
34
|
+
backgroundImage: `repeating-linear-gradient(100deg, ${dark ? "#000" : "#fff"} 0%, ${dark ? "#000" : "#fff"} 7%, transparent 10%, transparent 12%, ${dark ? "#000" : "#fff"} 16%), repeating-linear-gradient(100deg, ${stops}, ${colors[0]} 100%)`,
|
|
35
|
+
backgroundSize: "300%, 200%",
|
|
36
|
+
backgroundPosition: "50% 50%, 50% 50%",
|
|
37
|
+
maskImage: "radial-gradient(ellipse 120% 95% at 70% 0%, black 25%, transparent 78%)",
|
|
38
|
+
WebkitMaskImage: "radial-gradient(ellipse 120% 95% at 70% 0%, black 25%, transparent 78%)",
|
|
39
|
+
animation: `apex-aurora ${duration} linear infinite`,
|
|
40
|
+
mixBlendMode: dark ? "normal" : "multiply",
|
|
41
|
+
}}
|
|
42
|
+
/>
|
|
43
|
+
{children && <div className={`relative z-10 h-full w-full ${className}`}>{children}</div>}
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default AuroraBackground;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { motion, useReducedMotion } from "framer-motion";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Blur Reveal Text: words (or letters) rise out of a blur one after another
|
|
6
|
+
* when the text scrolls into view. An optional highlight word gets the accent.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const BlurRevealText = ({
|
|
10
|
+
text = "Design that moves people, built in minutes.",
|
|
11
|
+
as: Tag = "p",
|
|
12
|
+
by = "word",
|
|
13
|
+
stagger = 0.06,
|
|
14
|
+
duration = 0.7,
|
|
15
|
+
blur = 12,
|
|
16
|
+
highlight = [],
|
|
17
|
+
accentColor = "#a3e635",
|
|
18
|
+
once = true,
|
|
19
|
+
className = "",
|
|
20
|
+
}) => {
|
|
21
|
+
const reduced = useReducedMotion();
|
|
22
|
+
const words = text.split(" ");
|
|
23
|
+
let index = 0;
|
|
24
|
+
const MotionTag = motion[Tag] ?? motion.p;
|
|
25
|
+
|
|
26
|
+
const piece = (content, key, accent) => {
|
|
27
|
+
const delay = index++ * stagger;
|
|
28
|
+
return (
|
|
29
|
+
<motion.span
|
|
30
|
+
key={key}
|
|
31
|
+
className="inline-block will-change-[filter,transform]"
|
|
32
|
+
style={accent ? { color: accentColor } : undefined}
|
|
33
|
+
variants={{
|
|
34
|
+
hidden: reduced ? { opacity: 0 } : { opacity: 0, y: "0.35em", filter: `blur(${blur}px)` },
|
|
35
|
+
shown: { opacity: 1, y: 0, filter: "blur(0px)", transition: { duration, delay, ease: [0.22, 1, 0.36, 1] } },
|
|
36
|
+
}}
|
|
37
|
+
>
|
|
38
|
+
{content}
|
|
39
|
+
</motion.span>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<MotionTag aria-label={text} className={className} initial="hidden" whileInView="shown" viewport={{ once, amount: 0.4 }}>
|
|
45
|
+
{words.map((word, w) => {
|
|
46
|
+
const accent = highlight.some((h) => h.toLowerCase() === word.replace(/[^\p{L}\p{N}'-]/gu, "").toLowerCase());
|
|
47
|
+
return (
|
|
48
|
+
<React.Fragment key={w}>
|
|
49
|
+
<span aria-hidden="true" className="inline-block whitespace-nowrap">
|
|
50
|
+
{by === "letter" ? [...word].map((ch, c) => piece(ch, c, accent)) : piece(word, 0, accent)}
|
|
51
|
+
</span>
|
|
52
|
+
{w < words.length - 1 && " "}
|
|
53
|
+
</React.Fragment>
|
|
54
|
+
);
|
|
55
|
+
})}
|
|
56
|
+
</MotionTag>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export default BlurRevealText;
|