"use client"; /** * HERO — the signature moment. One orchestrated, choreographed page-load with * staggered reveals (the single highest-impact "alive" moment on the page). * - Real background VIDEO (autoplay/muted/loop/playsinline, poster, cover, * deferred load via IO, paused on hidden tab; reduced-motion -> poster only). * - ANIMATED SVG: a revenue growth line that DRAWS itself (GSAP DrawSVG) with * an area fill that fades up, grid ticks that draw in, and a "live" marker * that springs in then pulses. * - Headline revealed line-by-line behind a mask (clip + yPercent) with the * custom expo curve; eyebrow/sub/CTAs/trust stagger up with a blur mask. * - Pointer spotlight (spring-smoothed via CSS var) + parallax on scroll. * - Custom easing personality throughout; honors prefers-reduced-motion. */ import { useEffect, useRef } from "react"; import Link from "next/link"; import { gsap, SplitText } from "./gsap"; import Magnetic from "./Magnetic"; import FluidBackground from "./FluidBackground"; import { SITE } from "../content"; /* "emilOut" ease is registered once in ./gsap (shared across components). */ export default function Hero() { const root = useRef(null); const videoRef = useRef(null); // Spring-smoothed spotlight target so the glow trails the cursor with momentum // instead of snapping to raw pointer position. const target = useRef({ x: 50, y: 40 }); const current = useRef({ x: 50, y: 40 }); const onPointerMove = (e: React.PointerEvent) => { const el = root.current; if (!el) return; const r = el.getBoundingClientRect(); target.current.x = ((e.clientX - r.left) / r.width) * 100; target.current.y = ((e.clientY - r.top) / r.height) * 100; }; useEffect(() => { const el = root.current; if (!el) return; const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches; // --- spotlight: lerp current toward target each frame (momentum) --- let rafId = 0; if (!reduce) { const tick = () => { current.current.x += (target.current.x - current.current.x) * 0.08; current.current.y += (target.current.y - current.current.y) * 0.08; el.style.setProperty("--mx", `${current.current.x}%`); el.style.setProperty("--my", `${current.current.y}%`); rafId = requestAnimationFrame(tick); }; rafId = requestAnimationFrame(tick); } const video = videoRef.current; let io: IntersectionObserver | null = null; let onVis: (() => void) | null = null; if (!reduce) { if (video) { io = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) video.play().catch(() => {}); else video.pause(); }, { threshold: 0.1 } ); io.observe(video); onVis = () => { if (document.hidden) video.pause(); else if (video.getBoundingClientRect().top < window.innerHeight) video.play().catch(() => {}); }; document.addEventListener("visibilitychange", onVis); } const ctx = gsap.context(() => { // 1) Headline: split into lines+words, reveal each line from behind a // mask (overflow hidden on lines) with a deliberate stagger. const split = new SplitText(".hero__h1", { type: "lines,words", linesClass: "hero__line", }); // Apply the brand gradient to the final word ("revenue.") AFTER splitting. // A pre-nested got clipped by the line mask and never // revealed; as a normal split word it animates + shows correctly. split.words[split.words.length - 1]?.classList.add("grad"); gsap.set(".hero__h1", { autoAlpha: 1 }); gsap.set(".hero__line", { overflow: "hidden" }); // Fast, tight cascade so the FULL headline — including the last word // ("revenue.") — finishes well within ~600ms and is never left hidden. gsap.from(split.words, { yPercent: 118, duration: 0.5, ease: "emilOut", stagger: 0.025, delay: 0.12, }); // 2) Eyebrow, sub, CTAs, trust — lift + blur mask, tight stagger. gsap.set(".hero__stagger", { autoAlpha: 0 }); gsap.to(".hero__stagger", { autoAlpha: 1, y: 0, filter: "blur(0px)", duration: 1, ease: "emilOut", stagger: 0.07, delay: 0.55, startAt: { y: 26, filter: "blur(5px)" }, }); // 3) ANIMATED SVG — grid draws, the revenue line draws itself, area // fades up, the live marker springs in (back ease => overshoot). gsap.set(".hero-svg__area", { autoAlpha: 0 }); const tl = gsap.timeline({ delay: 0.45 }); tl.from(".hero-svg__grid line", { drawSVG: "0%", duration: 0.9, stagger: 0.06, ease: "power2.out", }) .from( ".hero-svg__line", { drawSVG: "0%", duration: 2, ease: "power2.inOut" }, "-=0.5" ) .to(".hero-svg__area", { autoAlpha: 1, duration: 1 }, "-=1.2") .from( ".hero-svg__dot", { scale: 0, transformOrigin: "center", duration: 0.6, ease: "back.out(2.2)", }, "-=0.5" ) .from( ".hero-svg__dot-ring", { scale: 0.4, autoAlpha: 0, transformOrigin: "center", duration: 0.6, ease: "emilOut" }, "<" ); // 4) Parallax: video drifts slower; content + SVG drift at different // rates as you scroll away => layered depth. gsap.to(".hero__media", { yPercent: 16, ease: "none", scrollTrigger: { trigger: el, start: "top top", end: "bottom top", scrub: 0.6 }, }); gsap.to(".hero-svg", { yPercent: 8, ease: "none", scrollTrigger: { trigger: el, start: "top top", end: "bottom top", scrub: 0.6 }, }); gsap.to(".hero__content", { yPercent: -10, opacity: 0.3, ease: "none", scrollTrigger: { trigger: el, start: "top top", end: "bottom top", scrub: 0.6 }, }); }, el); return () => { cancelAnimationFrame(rafId); io?.disconnect(); if (onVis) document.removeEventListener("visibilitychange", onVis); ctx.revert(); }; } // Reduced motion: reveal everything statically, show poster only. gsap.set([".hero__h1", ".hero__stagger", ".hero-svg__area"], { autoAlpha: 1, y: 0, filter: "none", }); return () => cancelAnimationFrame(rafId); }, []); return (
{/* ---- live WebGL fluid background (cursor-reactive) ---- */} {/* ---- background video (deferred) + poster + scrims ---- kept as a subtle moving TEXTURE layered over the fluid; the fluid is now the dominant, cursor-reactive element. */}
); }