agency-web/app/components/Hero.tsx

238 lines
8.2 KiB
TypeScript

"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<HTMLDivElement>(null);
const videoRef = useRef<HTMLVideoElement>(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 (
<section
ref={root}
className="hero"
aria-labelledby="hero-h1"
onPointerMove={onPointerMove}
>
{/* ---- background video (deferred) + poster + scrims ---- */}
<div className="hero__media" aria-hidden="true">
<video
ref={videoRef}
className="hero__video"
poster="/assets/hero-poster.jpg"
muted
loop
playsInline
preload="none"
>
<source src="/assets/hero.mp4" type="video/mp4" />
</video>
<div className="hero__scrim" />
<div className="hero__spotlight" />
</div>
{/* ---- animated SVG growth chart ---- */}
<svg
className="hero-svg"
viewBox="0 0 1200 600"
preserveAspectRatio="xMidYMid slice"
aria-hidden="true"
>
<defs>
<linearGradient id="heroLine" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stopColor="#3b82f6" />
<stop offset="55%" stopColor="#8b5cf6" />
<stop offset="100%" stopColor="#10b981" />
</linearGradient>
<linearGradient id="heroArea" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#8b5cf6" stopOpacity="0.35" />
<stop offset="100%" stopColor="#8b5cf6" stopOpacity="0" />
</linearGradient>
</defs>
<g className="hero-svg__grid">
<line x1="0" y1="150" x2="1200" y2="150" />
<line x1="0" y1="300" x2="1200" y2="300" />
<line x1="0" y1="450" x2="1200" y2="450" />
</g>
{/* area under the curve */}
<path
className="hero-svg__area"
fill="url(#heroArea)"
d="M0,520 C160,520 220,470 340,430 C470,388 520,300 660,290 C820,278 880,160 1040,120 C1110,102 1160,92 1200,86 L1200,600 L0,600 Z"
/>
{/* the revenue line that draws itself */}
<path
className="hero-svg__line"
fill="none"
stroke="url(#heroLine)"
strokeWidth="4"
strokeLinecap="round"
d="M0,520 C160,520 220,470 340,430 C470,388 520,300 660,290 C820,278 880,160 1040,120 C1110,102 1160,92 1200,86"
/>
<circle className="hero-svg__dot" cx="1200" cy="86" r="9" fill="#10b981" />
</svg>
{/* ---- content ---- */}
<div className="hero__content wrap">
<p className="hero__eyebrow hero__stagger">
<span className="hero__live" aria-hidden="true" />
Results-driven digital marketing agency
</p>
<h1 id="hero-h1" className="hero__h1">
Marketing that grows your <span className="grad">revenue.</span>
</h1>
<p className="hero__sub hero__stagger">
We run paid, SEO, and content programs built around your revenue
targets, then show you exactly what they returned. Every month.
</p>
<div className="hero__cta hero__stagger">
<Magnetic strength={0.4}>
<Link href={SITE.booking} className="btn btn--accent" data-cursor="Book it">
Get your growth audit
</Link>
</Magnetic>
<Magnetic strength={0.3}>
<a href="#work" className="btn btn--ghost" data-cursor="See proof">
See the results <span className="arrow" aria-hidden="true"></span>
</a>
</Magnetic>
</div>
<dl className="hero__trust hero__stagger">
<div>
<dt>$40M+</dt>
<dd>in client revenue generated</dd>
</div>
<div className="hero__trust-div" aria-hidden="true" />
<div>
<dt>50+</dt>
<dd>brands grown</dd>
</div>
</dl>
</div>
<a href="#manifesto" className="hero__scroll hero__stagger" aria-label="Scroll to content">
<span className="hero__scroll-line" aria-hidden="true" />
Scroll
</a>
</section>
);
}