apex-ui-kit 2.2.0 → 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/AnimatedSwitch/AnimatedSwitch.jsx +80 -0
- package/dist/ApexUI-Kit/templates/AnimatedTabs/AnimatedTabs.jsx +10 -2
- package/dist/ApexUI-Kit/templates/AuroraBackground/AuroraBackground.jsx +48 -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/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/ElasticSlider/ElasticSlider.jsx +152 -0
- package/dist/ApexUI-Kit/templates/ExtrudedText/ExtrudedText.jsx +78 -0
- package/dist/ApexUI-Kit/templates/FlipBook/FlipBook.jsx +191 -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/LoadingScreen/LoadingScreen.jsx +545 -101
- package/dist/ApexUI-Kit/templates/MagnetLines/MagnetLines.jsx +67 -0
- package/dist/ApexUI-Kit/templates/MagneticCursor/MagneticCursor.jsx +2 -160
- 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/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/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/TrueFocus/TrueFocus.jsx +64 -0
- package/dist/ApexUI-Kit/templates/TypewriterText/TypewriterText.jsx +70 -0
- package/dist/cli/index.js +43 -0
- package/package.json +1 -1
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import React, { useId, useState } from "react";
|
|
2
|
+
import { AnimatePresence, motion } from "framer-motion";
|
|
3
|
+
import { Check } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* Stepper: a multi-step flow. The indicators fill and tick as you move on,
|
|
7
|
+
* the connector lines fill in between, and each step's content slides in
|
|
8
|
+
* from the side you are heading while the panel height follows smoothly.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const DEFAULT_STEPS = [
|
|
12
|
+
{ title: "Account", content: <p>Welcome! Pick a username to get started — you can change it later.</p> },
|
|
13
|
+
{ title: "Profile", content: <p>Add a photo and a short bio so people know who you are.</p> },
|
|
14
|
+
{ title: "Theme", content: <p>Choose light, dark or follow your system. Your choice syncs everywhere.</p> },
|
|
15
|
+
{ title: "Done", content: <p>All set. Press Complete to finish onboarding.</p> },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const variants = {
|
|
19
|
+
enter: (dir) => ({ x: dir > 0 ? "40%" : "-40%", opacity: 0 }),
|
|
20
|
+
center: { x: 0, opacity: 1 },
|
|
21
|
+
exit: (dir) => ({ x: dir > 0 ? "-40%" : "40%", opacity: 0 }),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const Stepper = ({ steps = DEFAULT_STEPS, initialStep = 0, onStepChange, onComplete, accentColor = "#a3e635", theme = "dark", className = "" }) => {
|
|
25
|
+
const id = useId();
|
|
26
|
+
const [step, setStep] = useState(initialStep);
|
|
27
|
+
const [dir, setDir] = useState(1);
|
|
28
|
+
const [done, setDone] = useState(false);
|
|
29
|
+
const dark = theme !== "light";
|
|
30
|
+
const last = step === steps.length - 1;
|
|
31
|
+
|
|
32
|
+
const go = (to) => {
|
|
33
|
+
if (to < 0 || to >= steps.length) return;
|
|
34
|
+
setDir(to > step ? 1 : -1);
|
|
35
|
+
setStep(to);
|
|
36
|
+
onStepChange?.(to);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className={`w-full max-w-md rounded-[1.6rem] border p-6 ${dark ? "border-white/10 bg-neutral-950 text-white" : "border-black/10 bg-white text-neutral-900"} ${className}`}>
|
|
41
|
+
<div className="flex items-center">
|
|
42
|
+
{steps.map((s, i) => {
|
|
43
|
+
const complete = done || i < step;
|
|
44
|
+
const current = !done && i === step;
|
|
45
|
+
return (
|
|
46
|
+
<React.Fragment key={i}>
|
|
47
|
+
<button type="button" onClick={() => !done && go(i)} aria-label={`Step ${i + 1}: ${s.title}`} aria-current={current ? "step" : undefined} className="relative grid h-9 w-9 shrink-0 place-items-center rounded-full text-sm font-semibold">
|
|
48
|
+
<motion.span
|
|
49
|
+
className="absolute inset-0 rounded-full"
|
|
50
|
+
initial={false}
|
|
51
|
+
animate={{ backgroundColor: complete || current ? accentColor : dark ? "rgba(255,255,255,0.08)" : "rgba(0,0,0,0.06)", scale: current ? 1.08 : 1 }}
|
|
52
|
+
transition={{ duration: 0.3 }}
|
|
53
|
+
/>
|
|
54
|
+
{current && <motion.span layoutId={`${id}-ring`} className="absolute -inset-1 rounded-full border-2" style={{ borderColor: `color-mix(in srgb, ${accentColor} 45%, transparent)` }} />}
|
|
55
|
+
<span className={`relative ${complete || current ? "text-black" : "opacity-60"}`}>
|
|
56
|
+
{complete ? (
|
|
57
|
+
<motion.span initial={{ scale: 0, rotate: -45 }} animate={{ scale: 1, rotate: 0 }} className="block">
|
|
58
|
+
<Check className="h-4 w-4" strokeWidth={3} />
|
|
59
|
+
</motion.span>
|
|
60
|
+
) : (
|
|
61
|
+
i + 1
|
|
62
|
+
)}
|
|
63
|
+
</span>
|
|
64
|
+
</button>
|
|
65
|
+
{i < steps.length - 1 && (
|
|
66
|
+
<div className={`mx-2 h-1 flex-1 overflow-hidden rounded-full ${dark ? "bg-white/[0.08]" : "bg-black/[0.06]"}`}>
|
|
67
|
+
<motion.div className="h-full rounded-full" style={{ background: accentColor }} initial={false} animate={{ width: done || i < step ? "100%" : "0%" }} transition={{ duration: 0.4, ease: [0.22, 1, 0.36, 1] }} />
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
</React.Fragment>
|
|
71
|
+
);
|
|
72
|
+
})}
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<motion.div layout className="relative mt-6 overflow-hidden" transition={{ type: "spring", stiffness: 300, damping: 32 }}>
|
|
76
|
+
<AnimatePresence mode="popLayout" initial={false} custom={dir}>
|
|
77
|
+
<motion.div key={done ? "done" : step} custom={dir} variants={variants} initial="enter" animate="center" exit="exit" transition={{ type: "spring", stiffness: 300, damping: 32 }} className="pb-1">
|
|
78
|
+
{done ? (
|
|
79
|
+
<div className="py-4 text-center">
|
|
80
|
+
<p className="text-lg font-semibold">You are all set 🎉</p>
|
|
81
|
+
<p className={`mt-1 text-sm ${dark ? "text-white/55" : "text-neutral-500"}`}>Every step is complete.</p>
|
|
82
|
+
</div>
|
|
83
|
+
) : (
|
|
84
|
+
<>
|
|
85
|
+
<p className="text-[0.72rem] font-semibold uppercase tracking-[0.18em]" style={{ color: accentColor }}>
|
|
86
|
+
Step {step + 1} of {steps.length}
|
|
87
|
+
</p>
|
|
88
|
+
<h3 className="mt-1 text-xl font-semibold">{steps[step].title}</h3>
|
|
89
|
+
<div className={`mt-2 text-[0.92rem] leading-relaxed ${dark ? "text-white/65" : "text-neutral-600"}`}>{steps[step].content}</div>
|
|
90
|
+
</>
|
|
91
|
+
)}
|
|
92
|
+
</motion.div>
|
|
93
|
+
</AnimatePresence>
|
|
94
|
+
</motion.div>
|
|
95
|
+
|
|
96
|
+
<div className="mt-6 flex items-center justify-between">
|
|
97
|
+
{done ? (
|
|
98
|
+
<button type="button" onClick={() => {
|
|
99
|
+
setDone(false);
|
|
100
|
+
go(0);
|
|
101
|
+
}} className={`rounded-full px-4 py-2 text-sm font-medium ${dark ? "bg-white/[0.06] hover:bg-white/10" : "bg-black/[0.05] hover:bg-black/[0.08]"}`}>
|
|
102
|
+
Start over
|
|
103
|
+
</button>
|
|
104
|
+
) : (
|
|
105
|
+
<>
|
|
106
|
+
<button type="button" onClick={() => go(step - 1)} disabled={step === 0} className={`rounded-full px-4 py-2 text-sm font-medium transition disabled:pointer-events-none disabled:opacity-0 ${dark ? "text-white/70 hover:text-white" : "text-neutral-500 hover:text-neutral-900"}`}>
|
|
107
|
+
Back
|
|
108
|
+
</button>
|
|
109
|
+
<motion.button
|
|
110
|
+
whileTap={{ scale: 0.95 }}
|
|
111
|
+
type="button"
|
|
112
|
+
onClick={() => {
|
|
113
|
+
if (!last) return go(step + 1);
|
|
114
|
+
setDone(true);
|
|
115
|
+
onComplete?.();
|
|
116
|
+
}}
|
|
117
|
+
className="rounded-full px-5 py-2 text-sm font-semibold text-black"
|
|
118
|
+
style={{ background: accentColor }}
|
|
119
|
+
>
|
|
120
|
+
{last ? "Complete" : "Continue"}
|
|
121
|
+
</motion.button>
|
|
122
|
+
</>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export default Stepper;
|
|
@@ -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,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;
|
package/dist/cli/index.js
CHANGED
|
@@ -71,6 +71,49 @@ const componentMap = {
|
|
|
71
71
|
"toast-stack": "ToastStack",
|
|
72
72
|
"blur-reveal-text": "BlurRevealText",
|
|
73
73
|
"flip-clock": "FlipClock",
|
|
74
|
+
"cursor-trail": "CursorTrail",
|
|
75
|
+
"typewriter-text": "TypewriterText",
|
|
76
|
+
"image-compare": "ImageCompare",
|
|
77
|
+
"pricing-cards": "PricingCards",
|
|
78
|
+
"swipe-cards": "SwipeCards",
|
|
79
|
+
"animated-switch": "AnimatedSwitch",
|
|
80
|
+
"animated-switches": "AnimatedSwitch",
|
|
81
|
+
"aurora-background": "AuroraBackground",
|
|
82
|
+
"holo-card": "HoloCard",
|
|
83
|
+
"cube-gallery": "CubeGallery",
|
|
84
|
+
"flip-book": "FlipBook",
|
|
85
|
+
"extruded-text": "ExtrudedText",
|
|
86
|
+
"iso-stack": "IsoStack",
|
|
87
|
+
"floating-navbar": "FloatingNavbar",
|
|
88
|
+
"ai-chat": "AiChat",
|
|
89
|
+
"ai-prompt-box": "AiPromptBox",
|
|
90
|
+
"ai-thinking": "AiThinking",
|
|
91
|
+
"ai-voice-orb": "AiVoiceOrb",
|
|
92
|
+
"ai-response": "AiResponse",
|
|
93
|
+
"ai-model-prompt": "AiModelPrompt",
|
|
94
|
+
"hold-to-confirm": "HoldToConfirm",
|
|
95
|
+
"ai-voice-wave": "AiVoiceWave",
|
|
96
|
+
"ai-voice-bars": "AiVoiceBars",
|
|
97
|
+
"ai-voice-ring": "AiVoiceRing",
|
|
98
|
+
"ai-voice-blob": "AiVoiceBlob",
|
|
99
|
+
"shiny-text": "ShinyText",
|
|
100
|
+
"glitch-text": "GlitchText",
|
|
101
|
+
"rotating-text": "RotatingText",
|
|
102
|
+
"circular-text": "CircularText",
|
|
103
|
+
"proximity-text": "ProximityText",
|
|
104
|
+
"true-focus": "TrueFocus",
|
|
105
|
+
"text-ring-3d": "TextRing3D",
|
|
106
|
+
"star-border": "StarBorder",
|
|
107
|
+
"click-spark": "ClickSpark",
|
|
108
|
+
"elastic-slider": "ElasticSlider",
|
|
109
|
+
"stepper": "Stepper",
|
|
110
|
+
"coverflow-3d": "Coverflow3D",
|
|
111
|
+
"magnet-lines": "MagnetLines",
|
|
112
|
+
"pixel-transition": "PixelTransition",
|
|
113
|
+
"folder": "Folder",
|
|
114
|
+
"bounce-cards": "BounceCards",
|
|
115
|
+
"card-swap-3d": "CardSwap3D",
|
|
116
|
+
"gooey-nav": "GooeyNav",
|
|
74
117
|
"water-drop-reveal": "MaskView",
|
|
75
118
|
};
|
|
76
119
|
// Packages only some components need, installed on top of the shared ones.
|