"use client"; /** * HERO — the signature moment. Fixes the client's 4 complaints head-on: * (2) real background VIDEO (autoplay/muted/loop/playsinline, poster, cover, * deferred load, reduced-motion -> poster still only). * (3) ANIMATED SVG: a revenue growth line that draws itself (GSAP DrawSVG) * with an area fill that fades up and a pulsing "live" marker. * (1) real interaction: SplitText word reveal, cursor spotlight following the * pointer, magnetic CTAs, parallax on scroll. * (4) zero raster imagery beyond the video poster. */ import { useEffect, useRef } from "react"; import Link from "next/link"; import { gsap, SplitText } from "./gsap"; import Magnetic from "./Magnetic"; import { SITE } from "../content"; export default function Hero() { const root = useRef(null); const videoRef = useRef(null); // Pointer spotlight (CSS vars on the hero -> radial-gradient follows cursor). const onPointerMove = (e: React.PointerEvent) => { const el = root.current; if (!el) return; const r = el.getBoundingClientRect(); el.style.setProperty("--mx", `${((e.clientX - r.left) / r.width) * 100}%`); el.style.setProperty("--my", `${((e.clientY - r.top) / r.height) * 100}%`); }; useEffect(() => { const el = root.current; if (!el) return; const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches; // Defer the video: only start it once it's actually on screen + ready, // and pause it when the tab/section is hidden (saves battery + main thread). const video = videoRef.current; if (video && !reduce) { const io = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) video.play().catch(() => {}); else video.pause(); }, { threshold: 0.1 } ); io.observe(video); const 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 words/lines and reveal with a mask. const split = new SplitText(".hero__h1", { type: "lines,words" }); gsap.set(".hero__h1", { autoAlpha: 1 }); gsap.from(split.words, { yPercent: 120, opacity: 0, duration: 1, ease: "expo.out", stagger: 0.04, delay: 0.15, }); // 2) Staggered entrance for eyebrow, sub, CTAs, trust row. gsap.from(".hero__stagger", { y: 24, opacity: 0, duration: 0.9, ease: "expo.out", stagger: 0.08, delay: 0.5, }); // 3) ANIMATED SVG — the growth line draws itself, area fades in, // grid ticks pop, live marker pulses. gsap.set(".hero-svg__area", { autoAlpha: 0 }); const tl = gsap.timeline({ delay: 0.4 }); tl.from(".hero-svg__grid line", { drawSVG: "0%", duration: 0.8, stagger: 0.04, ease: "power2.out", }) .from( ".hero-svg__line", { drawSVG: "0%", duration: 1.8, ease: "power2.inOut" }, "-=0.4" ) .to(".hero-svg__area", { autoAlpha: 1, duration: 0.9 }, "-=1.1") .from( ".hero-svg__dot", { scale: 0, transformOrigin: "center", duration: 0.5, ease: "back.out(2)" }, "-=0.5" ); // 4) Parallax: video drifts slower than content as you scroll away. gsap.to(".hero__media", { yPercent: 18, ease: "none", scrollTrigger: { trigger: el, start: "top top", end: "bottom top", scrub: true }, }); gsap.to(".hero__content", { yPercent: -8, opacity: 0.4, ease: "none", scrollTrigger: { trigger: el, start: "top top", end: "bottom top", scrub: true }, }); }, el); return () => { io.disconnect(); document.removeEventListener("visibilitychange", onVis); ctx.revert(); }; } // Reduced motion: still reveal the SVG line statically (no draw), show poster. gsap.set([".hero__h1", ".hero__stagger", ".hero-svg__area"], { autoAlpha: 1 }); }, []); return (
{/* ---- background video (deferred) + poster + scrims ---- */}
); }