skills/website/templates/hero.tsx

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>
)
}