159 lines
4.6 KiB
TypeScript
159 lines
4.6 KiB
TypeScript
'use client'
|
|
|
|
// HeroSection.tsx — Cinematic hero with GSAP + ScrollTrigger
|
|
// Dependencies: gsap @gsap/react framer-motion
|
|
// Replace all brand.* references with values from src/lib/brand.ts
|
|
|
|
import { useRef } from 'react'
|
|
import { useGSAP } from '@gsap/react'
|
|
import gsap from 'gsap'
|
|
import { ScrollTrigger } from 'gsap/ScrollTrigger'
|
|
import { MagneticButton } from '@/components/ui/magnetic-button'
|
|
import { brand } from '@/lib/brand'
|
|
|
|
gsap.registerPlugin(ScrollTrigger)
|
|
|
|
export default function HeroSection() {
|
|
const containerRef = useRef<HTMLElement>(null)
|
|
|
|
useGSAP(
|
|
() => {
|
|
const tl = gsap.timeline()
|
|
|
|
// 1. Word-by-word headline reveal
|
|
tl.from('.word-inner', {
|
|
y: 60,
|
|
opacity: 0,
|
|
duration: 0.8,
|
|
stagger: 0.08,
|
|
ease: 'power4.out',
|
|
})
|
|
|
|
// 2. Subheadline fade-up
|
|
tl.from(
|
|
'.hero-sub',
|
|
{
|
|
y: 30,
|
|
opacity: 0,
|
|
duration: 0.7,
|
|
ease: 'power3.out',
|
|
},
|
|
'-=0.4'
|
|
)
|
|
|
|
// 3. CTA spring entrance
|
|
tl.from(
|
|
'.hero-cta-wrap',
|
|
{
|
|
scale: 0.9,
|
|
opacity: 0,
|
|
duration: 0.6,
|
|
ease: 'back.out(1.4)',
|
|
},
|
|
'-=0.3'
|
|
)
|
|
|
|
// 4. Ken-Burns background
|
|
gsap.to('.hero-bg', {
|
|
scale: 1.06,
|
|
duration: 8,
|
|
ease: 'none',
|
|
repeat: -1,
|
|
yoyo: true,
|
|
})
|
|
|
|
// 5. ScrollTrigger — pin + parallax exit
|
|
ScrollTrigger.create({
|
|
trigger: containerRef.current,
|
|
start: 'top top',
|
|
end: '+=80%',
|
|
pin: true,
|
|
scrub: 0.5,
|
|
onUpdate: (self) => {
|
|
gsap.set('.hero-content', {
|
|
y: self.progress * -60,
|
|
opacity: Math.max(0, 1 - self.progress * 2),
|
|
})
|
|
gsap.set('.hero-bg', {
|
|
scale: 1 + self.progress * 0.04,
|
|
})
|
|
},
|
|
})
|
|
},
|
|
{ scope: containerRef }
|
|
)
|
|
|
|
return (
|
|
<section
|
|
ref={containerRef}
|
|
className="relative grid min-h-[100dvh] grid-cols-1 overflow-hidden lg:grid-cols-[55fr_45fr]"
|
|
style={{ backgroundColor: 'var(--color-bg)' }}
|
|
>
|
|
{/* Left: Text content */}
|
|
<div className="hero-content relative z-10 flex flex-col justify-center px-8 py-24 lg:px-16 lg:py-32">
|
|
{/* Eyebrow tag */}
|
|
<span className="mb-6 inline-flex w-max items-center rounded-full border border-[var(--color-primary)]/20 bg-[var(--color-primary)]/8 px-3 py-1 text-[10px] font-medium uppercase tracking-[0.2em] text-[var(--color-primary)]">
|
|
{brand.industry}
|
|
</span>
|
|
|
|
{/* Headline with word-split spans */}
|
|
<h1 className="mb-6 text-5xl font-black leading-[0.95] tracking-tight text-[var(--color-secondary)] lg:text-7xl">
|
|
{brand.heroHeadline.split(' ').map((word, i) => (
|
|
<span
|
|
key={i}
|
|
className="word mr-[0.25em] inline-block overflow-hidden last:mr-0"
|
|
>
|
|
<span className="word-inner inline-block">{word}</span>
|
|
</span>
|
|
))}
|
|
</h1>
|
|
|
|
{/* Subheadline */}
|
|
<p className="hero-sub mb-10 max-w-md text-lg leading-relaxed text-[var(--color-secondary)]/60">
|
|
{brand.tagline}
|
|
</p>
|
|
|
|
{/* CTA */}
|
|
<div className="hero-cta-wrap">
|
|
<MagneticButton href="#contact">{brand.ctaText}</MagneticButton>
|
|
</div>
|
|
|
|
{/* Scroll indicator — no text, just a line */}
|
|
<div className="absolute bottom-10 left-8 hidden h-12 w-[1px] bg-[var(--color-secondary)]/20 lg:block" />
|
|
</div>
|
|
|
|
{/* Right: Visual — hidden on mobile, shown on lg+ */}
|
|
<div className="relative hidden lg:block">
|
|
<div
|
|
className="hero-bg absolute inset-0 origin-center bg-cover bg-center"
|
|
style={{
|
|
backgroundImage: `url(https://picsum.photos/seed/${encodeURIComponent(brand.name)}/900/1200)`,
|
|
}}
|
|
/>
|
|
{/* Gradient fade from bg color on left edge */}
|
|
<div
|
|
className="absolute inset-0"
|
|
style={{
|
|
background: `linear-gradient(to right, var(--color-bg) 0%, transparent 35%)`,
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{/* Mobile: decorative background strip at bottom */}
|
|
<div
|
|
className="relative h-64 w-full bg-cover bg-center lg:hidden"
|
|
style={{
|
|
backgroundImage: `url(https://picsum.photos/seed/${encodeURIComponent(brand.name)}/900/600)`,
|
|
}}
|
|
>
|
|
<div
|
|
className="absolute inset-0"
|
|
style={{
|
|
background: `linear-gradient(to bottom, var(--color-bg) 0%, transparent 40%)`,
|
|
}}
|
|
/>
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|