+
✨ LYTS WAY
LYTS SOCIAL CURRENCY
{[["DISCOVER", C.lime, "#0a0a0a"], ["ENGAGE", C.yellow, "#0a0a0a"], ["EARN LYTS", C.orange, "#fff"], ["SPEND", C.pink, "#fff"], ["LIVE MORE", "#fff", "#0a0a0a"], ["💞 LYTIAN", "#0a0a0a", C.lime]].map(([t, bg, tc]) => (
-
{t} {t !== "💞 LYTIAN" && "↓"}
+
{t} {t !== "💞 LYTIAN" && "↓"}
))}
@@ -394,7 +650,9 @@ export function Difference() {
);
}
-/* 09 — THE LOOP */
+/* 09 — THE LOOP
+ GSAP: the headline flips up line by line, then the six steps flip open
+ left-to-right like a chain of dominoes — the loop building itself. */
export function TheLoop() {
const cards = [
["01", "DISCOVER", "brands, places, people worth meeting.", C.pink, "#fff"],
@@ -404,23 +662,60 @@ export function TheLoop() {
["05", "LIVE MORE", "explore, connect, feel alive.", C.purple, "#fff"],
["06", "BECOME A LYTIAN", "the community you always wanted. 💞", "#0a0a0a", C.lime],
] as const;
+
+ useGSAP(() => {
+ syncLenisScrollTrigger();
+ const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
+ if (reduce) return;
+
+ gsap.set("#the-loop .loop-w-in", { yPercent: 118, rotationX: -85, autoAlpha: 0, transformOrigin: "50% 100%" });
+ gsap.set("#the-loop .loop-card", { rotationY: -92, xPercent: -6, autoAlpha: 0, transformOrigin: "left center" });
+
+ gsap.to("#the-loop .loop-w-in", {
+ scrollTrigger: { trigger: "#the-loop", start: "top 78%", once: true },
+ yPercent: 0,
+ rotationX: 0,
+ autoAlpha: 1,
+ stagger: 0.05,
+ duration: 0.8,
+ ease: "power4.out",
+ });
+
+ gsap.to("#the-loop .loop-card", {
+ scrollTrigger: { trigger: "#the-loop .loop-grid", start: "top 84%", once: true },
+ rotationY: 0,
+ xPercent: 0,
+ autoAlpha: 1,
+ stagger: 0.09,
+ duration: 0.7,
+ ease: "power3.out",
+ onComplete: () => gsap.set("#the-loop .loop-card", { clearProps: "transform" }),
+ });
+ }, []);
+
return (
🚀 your 6-step loop
-
- six small moments.
-
- one big life that keeps going.
-
-
+
+
+
+
+
+
+
+
{cards.map(([n, t, s, bg, tc]) => (
-
+
))}
-
+
♻️ every interaction creates value. every LYTIAN invites another.
);
diff --git a/src/components/story/SectionsB.tsx b/src/components/story/SectionsB.tsx
index c650296..0a83c50 100644
--- a/src/components/story/SectionsB.tsx
+++ b/src/components/story/SectionsB.tsx
@@ -1,3 +1,5 @@
+"use client";
+
import {
Section,
Pill,
@@ -6,11 +8,11 @@ import {
Heading,
Logo,
Group,
- Reveal,
Marquee,
ScrollLitText,
} from "./ui";
import { C } from "./palette";
+import { gsap, useGSAP, syncLenisScrollTrigger, MaskWords } from "./gsapkit";
/* 10 — FOR YOU */
export function ForYou() {
@@ -188,45 +190,76 @@ function ActionTile({
}) {
return (
+LYTs
{emoji}
-
{title}
-
{sub}
+
{title}
+
{sub}
);
}
-/* 14 — OLD vs NEW */
+/* 14 — OLD vs NEW
+ GSAP: a face-off. The headline reveals word by word, the two worlds charge
+ in from opposite edges, and the VS badge slams in with an elastic pop and a
+ quick shake — the clash — before each card's rule lines stagger up. */
export function OldVsNew() {
+ useGSAP(() => {
+ syncLenisScrollTrigger();
+ const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
+ if (reduce) return;
+
+ gsap.set("#old-vs-new .ovn-w-in", { yPercent: 120, autoAlpha: 0 });
+ gsap.set("#old-vs-new .ovn-left", { xPercent: -18, rotationZ: -4, autoAlpha: 0 });
+ gsap.set("#old-vs-new .ovn-right", { xPercent: 18, rotationZ: 4, autoAlpha: 0 });
+ gsap.set("#old-vs-new .ovn-vs", { scale: 0, autoAlpha: 0 });
+ gsap.set("#old-vs-new .ovn-rule", { y: 18, autoAlpha: 0 });
+
+ const tl = gsap.timeline({
+ scrollTrigger: { trigger: "#old-vs-new", start: "top 70%", once: true },
+ defaults: { ease: "power3.out" },
+ });
+ tl.to("#old-vs-new .ovn-w-in", { yPercent: 0, autoAlpha: 1, stagger: 0.05, duration: 0.7, ease: "power4.out" })
+ .to("#old-vs-new .ovn-left", { xPercent: 0, rotationZ: 0, autoAlpha: 1, duration: 0.7 }, "-=0.2")
+ .to("#old-vs-new .ovn-right", { xPercent: 0, rotationZ: 0, autoAlpha: 1, duration: 0.7 }, "<")
+ .to("#old-vs-new .ovn-vs", { scale: 1, autoAlpha: 1, duration: 0.6, ease: "elastic.out(1, 0.5)" }, "-=0.15")
+ .to("#old-vs-new .ovn-vs", { rotation: "+=8", duration: 0.08, yoyo: true, repeat: 3, ease: "power1.inOut" }, "<0.1")
+ .to("#old-vs-new .ovn-rule", { y: 0, autoAlpha: 1, stagger: 0.08, duration: 0.5 }, "-=0.3");
+ }, []);
+
return (
💞 a new kind of loyalty
traditional loyalty said one thing.
-
- LYTs says another.
-
+
+
+
+
-
+
💻 TRADITIONAL LOYALTY
- the old rule
spend first.
earn later.
+ the old rule
+ spend first.
+ earn later.
-
value is created only when you buy. participation = invisible. 🫥
-
-
-
VS
+
value is created only when you buy. participation = invisible. 🫥
-
+
+ VS
+
+
✨ THE LYTS WAY
- the new rule
live first.
earn everywhere.
+ the new rule
+ live first.
+ earn everywhere.
-
value is created when you participate. everyday life is the unlock. 🎉
+
value is created when you participate. everyday life is the unlock. 🎉
@@ -238,7 +271,10 @@ export function OldVsNew() {
);
}
-/* 15 — THE VISION */
+/* 15 — THE VISION
+ GSAP: the headline reveals word by word with "social" punching in on an
+ overshoot, while the value list stacks up — each row slides in from the
+ right and its ✓ ticks in with a springy pop, like a checklist completing. */
export function TheVision() {
const items = [
["every discovery has value", "#fff", "#0a0a0a"],
@@ -247,23 +283,52 @@ export function TheVision() {
["every community has value", C.orange, "#fff"],
["every person can live more", C.purple, "#fff"],
] as const;
+
+ useGSAP(() => {
+ syncLenisScrollTrigger();
+ const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
+ if (reduce) return;
+
+ gsap.set("#the-vision .vis-w-in", { yPercent: 118, autoAlpha: 0 });
+ gsap.set("#the-vision .vis-social", { scale: 1.6, autoAlpha: 0, transformOrigin: "0% 50%" });
+ gsap.set("#the-vision .vis-row", { xPercent: 26, autoAlpha: 0 });
+ gsap.set("#the-vision .vis-check", { scale: 0, rotation: -40, transformOrigin: "50% 50%" });
+
+ const tl = gsap.timeline({
+ scrollTrigger: { trigger: "#the-vision", start: "top 72%", once: true },
+ defaults: { ease: "power3.out" },
+ });
+ tl.to("#the-vision .vis-w-in", { yPercent: 0, autoAlpha: 1, stagger: 0.05, duration: 0.7, ease: "power4.out" })
+ .to("#the-vision .vis-social", { scale: 1, autoAlpha: 1, duration: 0.6, ease: "back.out(2.4)" }, "-=0.35")
+ .to("#the-vision .vis-row", { xPercent: 0, autoAlpha: 1, stagger: 0.11, duration: 0.6 }, "-=0.3")
+ .to("#the-vision .vis-check", { scale: 1, rotation: 0, stagger: 0.11, duration: 0.5, ease: "back.out(3)" }, "<0.15");
+ }, []);
+
return (
🌈 the vision
-
- world's largest social currency ecosystem.
+
+
+
+
+ social
+
+
+
- {items.map(([t, bg, tc], i) => (
-
-
- ✓
- {t}
-
-
+ {items.map(([t, bg, tc]) => (
+
+ ✓
+ {t}
+
))}
diff --git a/src/components/story/gsapkit.tsx b/src/components/story/gsapkit.tsx
new file mode 100644
index 0000000..489149d
--- /dev/null
+++ b/src/components/story/gsapkit.tsx
@@ -0,0 +1,105 @@
+"use client";
+
+/* Shared GSAP wiring for the story sections.
+ - Registers ScrollTrigger + the useGSAP helper once.
+ - Syncs Lenis smooth-scroll (set up in Providers) with ScrollTrigger so
+ scrubbed timelines stay glued to the smoothed scroll position.
+ - Exposes a pointer-parallax hook for depth-reactive floating objects. */
+
+import { useEffect, type RefObject } from "react";
+import gsap from "gsap";
+import { ScrollTrigger } from "gsap/ScrollTrigger";
+import { useGSAP } from "@gsap/react";
+
+if (typeof window !== "undefined") {
+ gsap.registerPlugin(ScrollTrigger, useGSAP);
+}
+
+/* Wire Lenis → ScrollTrigger.update exactly once. Lenis is created in a
+ Providers effect, so it may not exist yet on first call — retry on a few
+ frames until it appears, then stop. */
+let lenisSynced = false;
+export function syncLenisScrollTrigger() {
+ if (lenisSynced || typeof window === "undefined") return;
+ let tries = 0;
+ const attach = () => {
+ const lenis = (window as unknown as { lenis?: { on?: (e: string, cb: () => void) => void } }).lenis;
+ if (lenis?.on) {
+ lenis.on("scroll", ScrollTrigger.update);
+ lenisSynced = true;
+ ScrollTrigger.refresh();
+ return;
+ }
+ if (tries++ < 60) requestAnimationFrame(attach);
+ };
+ attach();
+}
+
+/* Depth-reactive pointer parallax: every element matching `selector` inside
+ `scope` drifts toward the cursor by its `data-depth` factor. Uses quickTo
+ for a buttery, GPU-friendly follow. Disabled under reduced-motion and on
+ touch (no hover). */
+export function usePointerParallax(
+ scope: RefObject,
+ selector: string,
+) {
+ useEffect(() => {
+ const root = scope.current;
+ if (!root) return;
+ if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
+ if (!window.matchMedia("(hover: hover)").matches) return;
+
+ const targets = Array.from(root.querySelectorAll(selector));
+ const movers = targets.map((el) => ({
+ x: gsap.quickTo(el, "x", { duration: 0.9, ease: "power3.out" }),
+ y: gsap.quickTo(el, "y", { duration: 0.9, ease: "power3.out" }),
+ depth: parseFloat(el.dataset.depth || "0.05"),
+ }));
+
+ const onMove = (e: PointerEvent) => {
+ const r = root.getBoundingClientRect();
+ const dx = (e.clientX - (r.left + r.width / 2));
+ const dy = (e.clientY - (r.top + r.height / 2));
+ movers.forEach((m) => {
+ m.x(dx * m.depth);
+ m.y(dy * m.depth);
+ });
+ };
+ const onLeave = () => movers.forEach((m) => { m.x(0); m.y(0); });
+
+ root.addEventListener("pointermove", onMove);
+ root.addEventListener("pointerleave", onLeave);
+ return () => {
+ root.removeEventListener("pointermove", onMove);
+ root.removeEventListener("pointerleave", onLeave);
+ };
+ }, [scope, selector]);
+}
+
+/* Splits a phrase into per-word masks: each word sits in an overflow-hidden
+ box so a GSAP tween on `.${wordClass}` can slide/flip it out of the mask.
+ `inner` lets a segment carry its own colour. */
+export function MaskWords({
+ text,
+ wordClass,
+ inner = "",
+}: {
+ text: string;
+ wordClass: string;
+ inner?: string;
+}) {
+ return (
+ <>
+ {text.split(" ").map((w, i) => (
+
+ {w}
+
+ ))}
+ >
+ );
+}
+
+export { gsap, ScrollTrigger, useGSAP };
diff --git a/src/components/story/ui.tsx b/src/components/story/ui.tsx
index 021efae..900b8aa 100644
--- a/src/components/story/ui.tsx
+++ b/src/components/story/ui.tsx
@@ -450,7 +450,7 @@ export function Pill({
}) {
return (