apex-ui-kit 2.2.0 → 2.3.1

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 (55) 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/AnimatedSwitch/AnimatedSwitch.jsx +80 -0
  14. package/dist/ApexUI-Kit/templates/AnimatedTabs/AnimatedTabs.jsx +12 -4
  15. package/dist/ApexUI-Kit/templates/AuroraBackground/AuroraBackground.jsx +48 -0
  16. package/dist/ApexUI-Kit/templates/BounceCards/BounceCards.jsx +53 -0
  17. package/dist/ApexUI-Kit/templates/CardSwap3D/CardSwap3D.jsx +80 -0
  18. package/dist/ApexUI-Kit/templates/CircularText/CircularText.jsx +49 -0
  19. package/dist/ApexUI-Kit/templates/ClickSpark/ClickSpark.jsx +74 -0
  20. package/dist/ApexUI-Kit/templates/Coverflow3D/Coverflow3D.jsx +108 -0
  21. package/dist/ApexUI-Kit/templates/CubeGallery/CubeGallery.jsx +168 -0
  22. package/dist/ApexUI-Kit/templates/CursorTrail/CursorTrail.jsx +200 -0
  23. package/dist/ApexUI-Kit/templates/DotGlobe/DotGlobe.jsx +1 -0
  24. package/dist/ApexUI-Kit/templates/DynamicIsland/DynamicIsland.jsx +1 -1
  25. package/dist/ApexUI-Kit/templates/ElasticSlider/ElasticSlider.jsx +161 -0
  26. package/dist/ApexUI-Kit/templates/ExtrudedText/ExtrudedText.jsx +78 -0
  27. package/dist/ApexUI-Kit/templates/FaqAccordion/FaqAccordion.jsx +1 -1
  28. package/dist/ApexUI-Kit/templates/FlipBook/FlipBook.jsx +191 -0
  29. package/dist/ApexUI-Kit/templates/FloatingNavbar/FloatingNavbar.jsx +162 -0
  30. package/dist/ApexUI-Kit/templates/Folder/Folder.jsx +101 -0
  31. package/dist/ApexUI-Kit/templates/GlitchText/GlitchText.jsx +56 -0
  32. package/dist/ApexUI-Kit/templates/GooeyNav/GooeyNav.jsx +312 -0
  33. package/dist/ApexUI-Kit/templates/HoldToConfirm/HoldToConfirm.jsx +169 -0
  34. package/dist/ApexUI-Kit/templates/HoldToConfirm/UndoNotice.jsx +58 -0
  35. package/dist/ApexUI-Kit/templates/HoloCard/HoloCard.jsx +92 -0
  36. package/dist/ApexUI-Kit/templates/ImageCompare/ImageCompare.jsx +97 -0
  37. package/dist/ApexUI-Kit/templates/IsoStack/IsoStack.jsx +86 -0
  38. package/dist/ApexUI-Kit/templates/LoadingScreen/LoadingScreen.jsx +545 -101
  39. package/dist/ApexUI-Kit/templates/MagnetLines/MagnetLines.jsx +67 -0
  40. package/dist/ApexUI-Kit/templates/MagneticCursor/MagneticCursor.jsx +2 -160
  41. package/dist/ApexUI-Kit/templates/PixelTransition/PixelTransition.jsx +75 -0
  42. package/dist/ApexUI-Kit/templates/PointerFollower/PointerFollower.jsx +1 -1
  43. package/dist/ApexUI-Kit/templates/PricingCards/PricingCards.jsx +115 -0
  44. package/dist/ApexUI-Kit/templates/ProximityText/ProximityText.jsx +66 -0
  45. package/dist/ApexUI-Kit/templates/RotatingText/RotatingText.jsx +50 -0
  46. package/dist/ApexUI-Kit/templates/ShinyText/ShinyText.jsx +36 -0
  47. package/dist/ApexUI-Kit/templates/StarBorder/StarBorder.jsx +33 -0
  48. package/dist/ApexUI-Kit/templates/Stepper/Stepper.jsx +142 -0
  49. package/dist/ApexUI-Kit/templates/SwipeCards/SwipeCards.jsx +123 -0
  50. package/dist/ApexUI-Kit/templates/TextRing3D/TextRing3D.jsx +75 -0
  51. package/dist/ApexUI-Kit/templates/TrueFocus/TrueFocus.jsx +64 -0
  52. package/dist/ApexUI-Kit/templates/TypewriterText/TypewriterText.jsx +70 -0
  53. package/dist/cli/index.js +43 -0
  54. package/package.json +1 -1
  55. package/dist/ApexUI-Kit/templates/Carousel/DragCarousel.jsx +0 -105
@@ -0,0 +1,191 @@
1
+ import React, { useEffect, useRef, useState } from "react";
2
+ import { animate, motion, useMotionValue, useTransform } from "framer-motion";
3
+ import { ChevronLeft, ChevronRight } from "lucide-react";
4
+
5
+ /*
6
+ * Flip Book: a small 3D hardcover book. Pages turn around the spine and lift
7
+ * off the stack while a shadow sweeps across them; drag a page to turn it
8
+ * by hand, click it, use the buttons or the arrow keys. The page edges
9
+ * thicken on whichever side has more pages, and every page is numbered.
10
+ */
11
+
12
+ const IK = "https://ik.imagekit.io/ny6yinyut/apexui/assets";
13
+ const DEFAULT_PAGES = [
14
+ { title: "ApexUI", body: "A field guide to motion", cover: true },
15
+ { title: "Chapter 1", body: "Start small. One spring, one easing, one idea per interaction." },
16
+ { title: "Chapter 2", body: "Motion should explain, where things come from and where they go.", image: `${IK}/spiderman.png?tr=f-auto,q-80,w-500` },
17
+ { title: "Chapter 3", body: "Keep it fast. Animate transform and opacity, never layout." },
18
+ { title: "Chapter 4", body: "Respect reduced motion. Calm is a feature too.", image: `${IK}/doctorStrange.png?tr=f-auto,q-80,w-500` },
19
+ { title: "Chapter 5", body: "Details compound. Tiny shadows and easing make it feel real." },
20
+ { title: "Chapter 6", body: "Test on a slow phone. If it stutters there, it is not done." },
21
+ { title: "The end", body: "Now go make something that feels alive.", cover: true },
22
+ ];
23
+
24
+ const PAPER = "#f7f2e8";
25
+
26
+ function Face({ page, number, accentColor, back }) {
27
+ const round = back ? "rounded-l-[0.7rem] rounded-r-sm" : "rounded-r-[0.7rem] rounded-l-sm";
28
+ if (!page) return <div className={`h-full w-full ${round}`} style={{ background: PAPER }} />;
29
+ if (page.cover) {
30
+ return (
31
+ <div className={`relative flex h-full w-full flex-col justify-between overflow-hidden p-6 text-neutral-950 ${round}`} style={{ background: `linear-gradient(135deg, ${accentColor}, color-mix(in srgb, ${accentColor} 50%, #000))` }}>
32
+ <div className="pointer-events-none absolute inset-3 rounded-md border border-black/15" />
33
+ <span className="text-[0.62rem] font-bold uppercase tracking-[0.3em] opacity-70">Volume 1</span>
34
+ <div>
35
+ <p className="text-[1.6rem] font-black leading-none">{page.title}</p>
36
+ <p className="mt-2 text-[0.82rem] font-medium opacity-80">{page.body}</p>
37
+ </div>
38
+ {/* Cloth texture */}
39
+ <div className="pointer-events-none absolute inset-0 opacity-[0.12] mix-blend-multiply" style={{ backgroundImage: "repeating-linear-gradient(45deg, #000 0 1px, transparent 1px 4px)" }} />
40
+ </div>
41
+ );
42
+ }
43
+ return (
44
+ <div className={`flex h-full w-full flex-col p-5 text-neutral-800 ${round}`} style={{ background: PAPER }}>
45
+ <p className="text-[0.62rem] font-bold uppercase tracking-[0.25em]" style={{ color: `color-mix(in srgb, ${accentColor} 50%, #000)` }}>
46
+ {page.title}
47
+ </p>
48
+ {page.image && <img src={page.image} alt="" draggable="false" className="mt-3 h-[38%] w-full rounded-md object-cover object-top" />}
49
+ <p className="mt-3 font-serif text-[0.98rem] leading-relaxed">{page.body}</p>
50
+ <div className="mt-auto flex items-end justify-between gap-3">
51
+ {!page.image && (
52
+ <div className="flex-1 space-y-1.5" aria-hidden="true">
53
+ {[90, 75, 82].map((w, i) => (
54
+ <span key={i} className="block h-1.5 rounded-full bg-neutral-900/10" style={{ width: `${w}%` }} />
55
+ ))}
56
+ </div>
57
+ )}
58
+ <span className={`font-mono text-[0.65rem] text-neutral-500 ${back ? "order-first" : "ml-auto"}`}>{number}</span>
59
+ </div>
60
+ </div>
61
+ );
62
+ }
63
+
64
+ // Stacked page edges peeking out under a page.
65
+ const edges = (count, side) => {
66
+ const n = Math.min(6, Math.ceil(count * 1.5));
67
+ const dir = side === "right" ? 1 : -1;
68
+ return Array.from({ length: n }, (_, k) => `${dir * (k + 1)}px ${k + 1}px 0 ${k % 2 ? "#e4dccb" : "#efe8da"}`).join(", ") || "none";
69
+ };
70
+
71
+ function Sheet({ index, sheet, flipped, active, dragAngle, accentColor, total, onClick }) {
72
+ const rot = useMotionValue(flipped ? -180 : 0);
73
+ // Lift while turning, and darken the page as it faces away from the light.
74
+ const z = useTransform(rot, [0, -90, -180], [0, 40, 0]);
75
+ const frontShade = useTransform(rot, [0, -90], [0, 0.45]);
76
+ const backShade = useTransform(rot, [-90, -180], [0.45, 0]);
77
+
78
+ useEffect(() => {
79
+ if (dragAngle != null) rot.set(dragAngle);
80
+ else animate(rot, flipped ? -180 : 0, { type: "spring", stiffness: 75, damping: 16 });
81
+ }, [flipped, dragAngle, rot]);
82
+
83
+ return (
84
+ <motion.div className="group absolute inset-0 origin-left cursor-pointer [transform-style:preserve-3d]" style={{ rotateY: rot, z, zIndex: active ? 50 : flipped ? index : total - index }} onClick={onClick}>
85
+ <div className="absolute inset-0 shadow-[0_18px_36px_-18px_rgba(0,0,0,0.55)] [backface-visibility:hidden]">
86
+ <Face page={sheet.front} number={index * 2 + 1} accentColor={accentColor} />
87
+ <div className="pointer-events-none absolute inset-y-0 left-0 w-10 bg-gradient-to-r from-black/20 via-black/5 to-transparent" />
88
+ <motion.div className="pointer-events-none absolute inset-0 rounded-r-[0.7rem] bg-gradient-to-l from-black to-black/40" style={{ opacity: frontShade }} />
89
+ {/* Dog-ear hint */}
90
+ {!flipped && <span className="pointer-events-none absolute bottom-0 right-0 h-7 w-7 origin-bottom-right scale-0 rounded-tl-md bg-gradient-to-tl from-transparent from-50% to-[#e2d9c6] to-50% shadow-[-2px_-2px_6px_rgba(0,0,0,0.15)] transition-transform duration-300 group-hover:scale-100" />}
91
+ </div>
92
+ <div className="absolute inset-0 shadow-[0_18px_36px_-18px_rgba(0,0,0,0.55)] [backface-visibility:hidden] [transform:rotateY(180deg)]">
93
+ <Face page={sheet.back} number={index * 2 + 2} accentColor={accentColor} back />
94
+ <div className="pointer-events-none absolute inset-y-0 right-0 w-10 bg-gradient-to-l from-black/20 via-black/5 to-transparent" />
95
+ <motion.div className="pointer-events-none absolute inset-0 rounded-l-[0.7rem] bg-gradient-to-r from-black to-black/40" style={{ opacity: backShade }} />
96
+ </div>
97
+ </motion.div>
98
+ );
99
+ }
100
+
101
+ const FlipBook = ({ pages = DEFAULT_PAGES, width = 12, accentColor = "#a3e635", className = "" }) => {
102
+ // Sheets: page 0+1 on sheet 0, 2+3 on sheet 1, …
103
+ const sheets = [];
104
+ for (let i = 0; i < pages.length; i += 2) sheets.push({ front: pages[i], back: pages[i + 1] });
105
+ const [turned, setTurned] = useState(0);
106
+ const [drag, setDrag] = useState(null); // { sheet, angle }
107
+ const start = useRef(null);
108
+ const dragged = useRef(false);
109
+ const pageRef = useRef(null);
110
+ const go = (dir) => setTurned((t) => Math.min(sheets.length, Math.max(0, t + dir)));
111
+ const open = turned > 0 && turned < sheets.length;
112
+
113
+ const onDown = (e) => {
114
+ dragged.current = false;
115
+ start.current = { x: e.clientX, moved: false, w: pageRef.current.offsetWidth };
116
+ };
117
+ const onMove = (e) => {
118
+ const s = start.current;
119
+ if (!s) return;
120
+ const dx = e.clientX - s.x;
121
+ if (!s.moved && Math.abs(dx) < 6) return;
122
+ if (!s.moved) {
123
+ s.moved = true;
124
+ s.sheet = dx < 0 ? turned : turned - 1;
125
+ s.from = dx < 0 ? 0 : -180;
126
+ if (s.sheet < 0 || s.sheet >= sheets.length) return (start.current = null);
127
+ e.currentTarget.setPointerCapture(e.pointerId);
128
+ }
129
+ const angle = Math.max(-180, Math.min(0, s.from + (dx / s.w) * 180));
130
+ setDrag({ sheet: s.sheet, angle });
131
+ };
132
+ const onUp = () => {
133
+ const s = start.current;
134
+ start.current = null;
135
+ if (!s?.moved || !drag) return;
136
+ dragged.current = true;
137
+ const flippedNow = drag.angle < -90;
138
+ setTurned(flippedNow ? s.sheet + 1 : s.sheet);
139
+ setDrag(null);
140
+ };
141
+
142
+ return (
143
+ <div
144
+ className={`flex flex-col items-center gap-7 outline-none ${className}`}
145
+ tabIndex={0}
146
+ onKeyDown={(e) => {
147
+ if (e.key === "ArrowRight") go(1);
148
+ if (e.key === "ArrowLeft") go(-1);
149
+ }}
150
+ aria-label="Flip book"
151
+ >
152
+ <div className="relative flex touch-pan-y select-none [perspective:2000px]" style={{ width: `min(${width * 2}rem, 92vw)` }} onPointerDown={onDown} onPointerMove={onMove} onPointerUp={onUp} onPointerCancel={onUp}>
153
+ {/* Hardcover boards behind the open spread */}
154
+ <div className="pointer-events-none absolute -inset-x-2 -inset-y-2 rounded-xl transition-opacity duration-500" style={{ opacity: open ? 1 : 0, background: `color-mix(in srgb, ${accentColor} 35%, #111)`, boxShadow: "0 30px 60px -25px rgba(0,0,0,0.6)" }} />
155
+ <div className="flex-1" />
156
+ <div ref={pageRef} className="relative aspect-[3/4] flex-1 [transform-style:preserve-3d]" style={{ transform: `translateX(${turned === 0 && !drag ? "-50%" : turned === sheets.length && !drag ? "50%" : "0%"})`, transition: "transform 600ms cubic-bezier(0.22,1,0.36,1)" }}>
157
+ <div className="absolute inset-y-0 right-full w-full rounded-l-[0.7rem] transition-[opacity,box-shadow] duration-500" style={{ opacity: turned > 0 ? 1 : 0, boxShadow: edges(turned, "left") }} />
158
+ <div className="absolute inset-0 rounded-r-[0.7rem] transition-[opacity,box-shadow] duration-500" style={{ opacity: turned < sheets.length ? 1 : 0, boxShadow: edges(sheets.length - turned, "right") }} />
159
+ {sheets.map((sheet, i) => (
160
+ <Sheet
161
+ key={i}
162
+ index={i}
163
+ sheet={sheet}
164
+ total={sheets.length}
165
+ flipped={i < turned}
166
+ active={drag?.sheet === i}
167
+ dragAngle={drag?.sheet === i ? drag.angle : null}
168
+ accentColor={accentColor}
169
+ onClick={() => !dragged.current && go(i < turned ? -1 : 1)}
170
+ />
171
+ ))}
172
+ </div>
173
+ </div>
174
+ <div className="flex items-center gap-4">
175
+ <button type="button" aria-label="Previous page" onClick={() => go(-1)} disabled={turned === 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">
176
+ <ChevronLeft className="h-5 w-5" />
177
+ </button>
178
+ <div className="flex gap-1" aria-hidden="true">
179
+ {Array.from({ length: sheets.length + 1 }, (_, i) => (
180
+ <span key={i} className="h-1.5 rounded-full transition-all duration-300" style={{ width: i === turned ? 18 : 6, background: i === turned ? accentColor : "color-mix(in srgb, currentColor 25%, transparent)" }} />
181
+ ))}
182
+ </div>
183
+ <button type="button" aria-label="Next page" onClick={() => go(1)} disabled={turned === sheets.length} 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">
184
+ <ChevronRight className="h-5 w-5" />
185
+ </button>
186
+ </div>
187
+ </div>
188
+ );
189
+ };
190
+
191
+ export default FlipBook;
@@ -0,0 +1,162 @@
1
+ import React, { useEffect, useId, useState } from "react";
2
+ import { AnimatePresence, motion } from "framer-motion";
3
+ import { ArrowUpRight } from "lucide-react";
4
+
5
+ /*
6
+ * Floating Navbar: a glassy pill that floats over the page. A highlight
7
+ * follows the pointer across the links, the active link keeps a dot, and the
8
+ * bar tucks away when you scroll down and slides back when you scroll up.
9
+ * It switches to a hamburger menu based on its own width (container query),
10
+ * so it is responsive wherever you put it.
11
+ */
12
+
13
+ const DEFAULT_LINKS = [
14
+ { label: "Home", href: "#home" },
15
+ { label: "Components", href: "#components" },
16
+ { label: "Templates", href: "#templates" },
17
+ { label: "Pricing", href: "#pricing" },
18
+ { label: "Blog", href: "#blog" },
19
+ ];
20
+
21
+ function Burger({ open }) {
22
+ return (
23
+ <span className="relative block h-3.5 w-5" aria-hidden="true">
24
+ <motion.span className="absolute left-0 top-0 h-[2px] w-full rounded-full bg-current" animate={open ? { top: "50%", rotate: 45, y: "-50%" } : { top: "0%", rotate: 0, y: 0 }} />
25
+ <motion.span className="absolute left-0 top-1/2 h-[2px] w-full -translate-y-1/2 rounded-full bg-current" animate={{ opacity: open ? 0 : 1, scaleX: open ? 0.3 : 1 }} />
26
+ <motion.span className="absolute bottom-0 left-0 h-[2px] w-full rounded-full bg-current" animate={open ? { bottom: "50%", rotate: -45, y: "50%" } : { bottom: "0%", rotate: 0, y: 0 }} />
27
+ </span>
28
+ );
29
+ }
30
+
31
+ const FloatingNavbar = ({
32
+ brand = "Apex",
33
+ links = DEFAULT_LINKS,
34
+ cta = { label: "Get started", href: "#start" },
35
+ active: activeProp,
36
+ onNavigate,
37
+ hideOnScroll = true,
38
+ scrollRoot,
39
+ accentColor = "#a3e635",
40
+ theme = "dark",
41
+ className = "",
42
+ }) => {
43
+ const id = useId();
44
+ const [active, setActive] = useState(activeProp ?? links[0]?.label);
45
+ const [hover, setHover] = useState(null);
46
+ const [open, setOpen] = useState(false);
47
+ const [hidden, setHidden] = useState(false);
48
+ const [compact, setCompact] = useState(false);
49
+ const dark = theme !== "light";
50
+
51
+ // Hide on scroll down, show on scroll up; compact once the page has moved.
52
+ useEffect(() => {
53
+ const root = scrollRoot?.current ?? window;
54
+ const read = () => (root === window ? window.scrollY : root.scrollTop);
55
+ let last = read();
56
+ const onScroll = () => {
57
+ const y = read();
58
+ setCompact(y > 24);
59
+ if (hideOnScroll && Math.abs(y - last) > 6) setHidden(y > last && y > 80);
60
+ last = y;
61
+ };
62
+ root.addEventListener("scroll", onScroll, { passive: true });
63
+ return () => root.removeEventListener("scroll", onScroll);
64
+ }, [hideOnScroll, scrollRoot]);
65
+
66
+ const go = (link, e) => {
67
+ setActive(link.label);
68
+ setOpen(false);
69
+ onNavigate?.(link, e);
70
+ };
71
+
72
+ const surface = dark ? "border-white/10 bg-neutral-900/70 text-white shadow-[0_10px_40px_-12px_rgba(0,0,0,0.7)]" : "border-black/10 bg-white/75 text-neutral-900 shadow-[0_10px_40px_-14px_rgba(0,0,0,0.25)]";
73
+ const muted = dark ? "text-white/65 hover:text-white" : "text-neutral-500 hover:text-neutral-900";
74
+
75
+ return (
76
+ <div className={`@container sticky top-3 z-50 w-full px-3 ${className}`}>
77
+ <motion.nav
78
+ aria-label="Main"
79
+ initial={false}
80
+ animate={{ y: hidden && !open ? "-140%" : "0%", opacity: hidden && !open ? 0 : 1 }}
81
+ transition={{ type: "spring", stiffness: 380, damping: 34 }}
82
+ className={`mx-auto max-w-4xl rounded-[1.6rem] border backdrop-blur-xl transition-[padding] duration-300 ${surface} ${compact ? "px-2.5 py-1.5" : "px-3 py-2.5"}`}
83
+ >
84
+ <div className="flex items-center justify-between gap-3">
85
+ <a href="#home" onClick={(e) => go(links[0], e)} className="flex items-center gap-2 pl-1.5 text-[0.95rem] font-bold">
86
+ <motion.span whileHover={{ rotate: -12, scale: 1.08 }} className="grid h-8 w-8 place-items-center rounded-xl text-sm font-black text-black" style={{ background: accentColor }}>
87
+ {brand[0]}
88
+ </motion.span>
89
+ {brand}
90
+ </a>
91
+
92
+ {/* Desktop links with a pill that follows the pointer. */}
93
+ <ul className="hidden items-center gap-0.5 @xl:flex" onPointerLeave={() => setHover(null)}>
94
+ {links.map((link) => {
95
+ const on = active === link.label;
96
+ return (
97
+ <li key={link.label} className="relative">
98
+ <a
99
+ href={link.href}
100
+ onClick={(e) => go(link, e)}
101
+ onPointerEnter={() => setHover(link.label)}
102
+ aria-current={on ? "page" : undefined}
103
+ className={`relative block rounded-full px-3 py-2 text-[0.86rem] font-medium transition-colors ${on ? "" : muted}`}
104
+ >
105
+ {hover === link.label && <motion.span layoutId={`${id}-hover`} className={`absolute inset-0 rounded-full ${dark ? "bg-white/[0.08]" : "bg-black/[0.05]"}`} transition={{ type: "spring", stiffness: 500, damping: 38 }} />}
106
+ <span className="relative">{link.label}</span>
107
+ {on && <motion.span layoutId={`${id}-dot`} className="absolute bottom-0.5 left-1/2 h-1 w-1 -translate-x-1/2 rounded-full" style={{ background: accentColor }} />}
108
+ </a>
109
+ </li>
110
+ );
111
+ })}
112
+ </ul>
113
+
114
+ <div className="flex items-center gap-1.5">
115
+ {cta && (
116
+ <motion.a
117
+ href={cta.href}
118
+ whileHover={{ scale: 1.04 }}
119
+ whileTap={{ scale: 0.96 }}
120
+ className="hidden items-center gap-1 rounded-full px-4 py-2 text-[0.85rem] font-semibold text-black @3xl:inline-flex"
121
+ style={{ background: accentColor }}
122
+ >
123
+ {cta.label}
124
+ <ArrowUpRight className="h-4 w-4" />
125
+ </motion.a>
126
+ )}
127
+ <button type="button" aria-label={open ? "Close menu" : "Open menu"} aria-expanded={open} onClick={() => setOpen((o) => !o)} className={`grid h-10 w-10 place-items-center rounded-full @xl:hidden ${dark ? "hover:bg-white/10" : "hover:bg-black/5"}`}>
128
+ <Burger open={open} />
129
+ </button>
130
+ </div>
131
+ </div>
132
+
133
+ {/* Mobile menu */}
134
+ <AnimatePresence initial={false}>
135
+ {open && (
136
+ <motion.div initial={{ height: 0, opacity: 0 }} animate={{ height: "auto", opacity: 1 }} exit={{ height: 0, opacity: 0 }} transition={{ duration: 0.3, ease: [0.22, 1, 0.36, 1] }} className="overflow-hidden @xl:hidden">
137
+ <ul className="space-y-1 px-1 pb-2 pt-3">
138
+ {links.map((link, i) => (
139
+ <motion.li key={link.label} initial={{ x: -14, opacity: 0 }} animate={{ x: 0, opacity: 1 }} transition={{ delay: 0.04 * i + 0.05 }}>
140
+ <a href={link.href} onClick={(e) => go(link, e)} className={`flex items-center justify-between rounded-2xl px-4 py-3 text-[0.95rem] font-medium ${active === link.label ? (dark ? "bg-white/[0.08]" : "bg-black/[0.05]") : ""}`}>
141
+ {link.label}
142
+ {active === link.label && <span className="h-1.5 w-1.5 rounded-full" style={{ background: accentColor }} />}
143
+ </a>
144
+ </motion.li>
145
+ ))}
146
+ {cta && (
147
+ <motion.li initial={{ y: 8, opacity: 0 }} animate={{ y: 0, opacity: 1 }} transition={{ delay: 0.04 * links.length + 0.08 }} className="pt-2">
148
+ <a href={cta.href} className="flex items-center justify-center gap-1 rounded-2xl py-3 text-[0.95rem] font-semibold text-black" style={{ background: accentColor }}>
149
+ {cta.label} <ArrowUpRight className="h-4 w-4" />
150
+ </a>
151
+ </motion.li>
152
+ )}
153
+ </ul>
154
+ </motion.div>
155
+ )}
156
+ </AnimatePresence>
157
+ </motion.nav>
158
+ </div>
159
+ );
160
+ };
161
+
162
+ export default FloatingNavbar;
@@ -0,0 +1,101 @@
1
+ import React, { useState } from "react";
2
+ import { motion } from "framer-motion";
3
+
4
+ /*
5
+ * Folder: a little folder that opens on click — the front flap tilts down
6
+ * and up to three papers fan out of it. While open, each paper leans toward
7
+ * the pointer a little. Click again to tuck everything back in.
8
+ */
9
+
10
+ const DEFAULT_ITEMS = [
11
+ <div key="a" className="space-y-1.5 p-2">
12
+ <p className="text-[0.55rem] font-bold text-neutral-800">Notes.md</p>
13
+ {[85, 60, 72].map((w) => (
14
+ <span key={w} className="block h-1 rounded-full bg-neutral-300" style={{ width: `${w}%` }} />
15
+ ))}
16
+ </div>,
17
+ <div key="b" className="space-y-1.5 p-2">
18
+ <p className="text-[0.55rem] font-bold text-neutral-800">Plan.pdf</p>
19
+ {[70, 90, 50].map((w) => (
20
+ <span key={w} className="block h-1 rounded-full bg-neutral-300" style={{ width: `${w}%` }} />
21
+ ))}
22
+ </div>,
23
+ <div key="c" className="space-y-1.5 p-2">
24
+ <p className="text-[0.55rem] font-bold text-neutral-800">Logo.png</p>
25
+ <span className="block h-7 rounded bg-gradient-to-br from-lime-300 to-cyan-300" />
26
+ </div>,
27
+ ];
28
+
29
+ // Where each paper lands when open, in em (scales with `size`).
30
+ const SPOTS = [
31
+ { x: -3.9, y: -3.3, r: -14 },
32
+ { x: 0, y: -4.2, r: 3 },
33
+ { x: 3.9, y: -3.3, r: 15 },
34
+ ];
35
+
36
+ const Folder = ({ color = "#a3e635", size = 1, items = DEFAULT_ITEMS, label = "Projects", className = "" }) => {
37
+ const [open, setOpen] = useState(false);
38
+ const [lean, setLean] = useState([]);
39
+ const papers = items.slice(0, 3);
40
+ const back = `color-mix(in srgb, ${color} 75%, #000)`;
41
+
42
+ return (
43
+ <div className={`flex flex-col items-center gap-4 ${className}`} style={{ fontSize: `${size}rem` }}>
44
+ <button
45
+ type="button"
46
+ aria-expanded={open}
47
+ aria-label={`${label} folder`}
48
+ onClick={() => {
49
+ setOpen((o) => !o);
50
+ setLean([]);
51
+ }}
52
+ className="group relative h-[5.5em] w-[7em] outline-none transition-transform duration-200 hover:-translate-y-1"
53
+ style={{ perspective: "600px" }}
54
+ >
55
+ {/* Back panel with tab */}
56
+ <span className="absolute inset-0 rounded-[0.6em] rounded-tl-none" style={{ background: back }} />
57
+ <span className="absolute -top-[0.7em] left-0 h-[0.9em] w-[2.6em] rounded-t-[0.45em]" style={{ background: back }} />
58
+
59
+ {papers.map((p, i) => {
60
+ const spot = SPOTS[papers.length === 1 ? 1 : papers.length === 2 ? i * 2 : i];
61
+ const l = lean[i] ?? { x: 0, y: 0 };
62
+ return (
63
+ <motion.span
64
+ key={i}
65
+ className="absolute bottom-[10%] left-[10%] h-[80%] w-[80%] overflow-hidden rounded-[0.4em] bg-white text-left shadow-md"
66
+ initial={false}
67
+ animate={open ? { x: `${spot.x + l.x}em`, y: `${spot.y + l.y}em`, rotate: spot.r, scale: 1 } : { x: "0em", y: `${-0.3 - i * 0.2}em`, rotate: 0, scale: 0.92 - (2 - i) * 0.03 }}
68
+ transition={{ type: "spring", stiffness: 300, damping: 22, delay: open ? i * 0.04 : 0 }}
69
+ onPointerMove={(e) => {
70
+ if (!open) return;
71
+ const r = e.currentTarget.getBoundingClientRect();
72
+ const next = [...lean];
73
+ next[i] = { x: ((e.clientX - (r.left + r.width / 2)) / r.width) * 0.8, y: ((e.clientY - (r.top + r.height / 2)) / r.height) * 0.8 };
74
+ setLean(next);
75
+ }}
76
+ onPointerLeave={() => {
77
+ const next = [...lean];
78
+ next[i] = { x: 0, y: 0 };
79
+ setLean(next);
80
+ }}
81
+ >
82
+ {p}
83
+ </motion.span>
84
+ );
85
+ })}
86
+
87
+ {/* Front flap */}
88
+ <motion.span
89
+ className="absolute inset-x-0 bottom-0 h-[85%] origin-bottom rounded-[0.6em] shadow-[0_-4px_12px_rgba(0,0,0,0.12)]"
90
+ style={{ background: `linear-gradient(180deg, color-mix(in srgb, ${color} 85%, #fff), ${color})` }}
91
+ initial={false}
92
+ animate={{ rotateX: open ? -38 : 0 }}
93
+ transition={{ type: "spring", stiffness: 260, damping: 20 }}
94
+ />
95
+ </button>
96
+ <span className="text-sm font-medium opacity-70">{label}</span>
97
+ </div>
98
+ );
99
+ };
100
+
101
+ export default Folder;
@@ -0,0 +1,56 @@
1
+ import React, { useEffect, useRef, useState } from "react";
2
+ import { useReducedMotion } from "framer-motion";
3
+
4
+ /*
5
+ * Glitch Text: two colored copies of the text sit behind it and jump
6
+ * around in random horizontal slices, like a broken signal. Runs all the
7
+ * time or only while hovered.
8
+ */
9
+
10
+ const slice = () => {
11
+ const top = Math.random() * 90;
12
+ return `inset(${top}% 0 ${Math.max(0, 100 - top - (6 + Math.random() * 22))}% 0)`;
13
+ };
14
+
15
+ const GlitchText = ({ text = "GLITCH", colors = ["#22d3ee", "#f43f5e"], speed = 90, hoverOnly = false, className = "" }) => {
16
+ const reduced = useReducedMotion();
17
+ const [hover, setHover] = useState(false);
18
+ const a = useRef(null);
19
+ const b = useRef(null);
20
+ const active = !reduced && (!hoverOnly || hover);
21
+
22
+ useEffect(() => {
23
+ const layers = [a.current, b.current];
24
+ if (!active) {
25
+ layers.forEach((el) => {
26
+ el.style.clipPath = "inset(50% 0 50% 0)";
27
+ el.style.transform = "none";
28
+ });
29
+ return undefined;
30
+ }
31
+ const id = setInterval(() => {
32
+ layers.forEach((el, i) => {
33
+ const burst = Math.random() < 0.25;
34
+ el.style.clipPath = slice();
35
+ el.style.transform = `translate(${(i ? 1 : -1) * (burst ? 6 + Math.random() * 6 : 1 + Math.random() * 3)}px, ${(Math.random() - 0.5) * 2}px)`;
36
+ });
37
+ }, speed);
38
+ return () => clearInterval(id);
39
+ }, [active, speed]);
40
+
41
+ const layer = "pointer-events-none absolute inset-0 select-none";
42
+ return (
43
+ <span className={`relative inline-block font-black tracking-tight ${className}`} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)}>
44
+ <span>{text}</span>
45
+ {/* Sliced, shifted copies painted over the text. */}
46
+ <span ref={a} aria-hidden="true" className={layer} style={{ color: colors[0], clipPath: "inset(50% 0 50% 0)" }}>
47
+ {text}
48
+ </span>
49
+ <span ref={b} aria-hidden="true" className={layer} style={{ color: colors[1], clipPath: "inset(50% 0 50% 0)" }}>
50
+ {text}
51
+ </span>
52
+ </span>
53
+ );
54
+ };
55
+
56
+ export default GlitchText;