# Phase 2 — Cinematic Hero Animation
Generate the hero section using GSAP + ScrollTrigger. Uses the brand data from Phase 1.
## Dependencies
```bash
npm install gsap @gsap/react lenis
```
Register plugins once at app root (in `layout.tsx` or a client provider):
```tsx
'use client'
import { useEffect } from 'react'
import gsap from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
export function GSAPProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
gsap.registerPlugin(ScrollTrigger)
}, [])
return <>{children}>
}
```
Wrap `
` with `` in `layout.tsx`.
## Animation Sequence
Execute these in order inside `useGSAP(() => { ... }, { scope: containerRef })`:
### 1. Hero Headline — Word Stagger Reveal
```ts
// Split headline into word spans before rendering
// In JSX: {headline.split(' ').map((w, i) => {w} )}
gsap.from('.word-inner', {
y: 60,
opacity: 0,
duration: 0.8,
stagger: 0.08,
ease: 'power4.out',
})
```
### 2. Subheadline — Fade Up
```ts
gsap.from('.hero-sub', {
y: 30,
opacity: 0,
duration: 0.7,
delay: 0.4,
ease: 'power3.out',
})
```
### 3. CTA Button — Spring Entrance
```ts
gsap.from('.hero-cta', {
scale: 0.9,
opacity: 0,
duration: 0.6,
delay: 0.7,
ease: 'back.out(1.4)',
})
```
### 4. Background — Ken Burns (infinite)
```ts
gsap.to('.hero-bg', {
scale: 1.06,
duration: 8,
ease: 'none',
repeat: -1,
yoyo: true,
})
```
### 5. ScrollTrigger — Pin + Parallax Exit
```ts
ScrollTrigger.create({
trigger: containerRef.current,
start: 'top top',
end: '+=100%',
pin: true,
scrub: 0.5,
onUpdate: (self) => {
gsap.set('.hero-content', { y: self.progress * -80, opacity: 1 - self.progress * 1.5 })
gsap.set('.hero-bg', { scale: 1 + self.progress * 0.06 })
},
})
```
## Magnetic CTA Button
The CTA uses Framer Motion `useMotionValue` for magnetic pull. Install: `npm install framer-motion`
```tsx
'use client'
import { useRef } from 'react'
import { motion, useMotionValue, useSpring, useTransform } from 'framer-motion'
export function MagneticButton({ children, href }: { children: React.ReactNode; href: string }) {
const ref = useRef(null)
const x = useMotionValue(0)
const y = useMotionValue(0)
const springX = useSpring(x, { stiffness: 150, damping: 15 })
const springY = useSpring(y, { stiffness: 150, damping: 15 })
const handleMouseMove = (e: React.MouseEvent) => {
const rect = ref.current!.getBoundingClientRect()
const cx = rect.left + rect.width / 2
const cy = rect.top + rect.height / 2
x.set((e.clientX - cx) * 0.3)
y.set((e.clientY - cy) * 0.3)
}
const handleMouseLeave = () => {
x.set(0)
y.set(0)
}
return (
{children}
↗
)
}
```
## Layout: Asymmetric Split
Always: text left (55%), visual right (45%). Never centered.
```tsx
{/* Left: Text content */}
{/* Eyebrow tag */}
{brand.industry}
{/* Headline with word spans */}
{brand.heroHeadline.split(' ').map((word, i) => (
{word}
))}
{/* Subheadline */}
{brand.tagline}
{/* CTA */}
{brand.ctaText}
{/* Right: Visual */}
```
## CSS Variables
In `globals.css`, inject brand tokens:
```css
:root {
--color-primary: ;
--color-secondary: ;
--color-accent: ;
--color-bg: ;
}
```
## Mobile Collapse
Below `lg:` breakpoint:
- Grid becomes single column (`grid-cols-1`)
- Background image moved to a decorative strip below headline
- All `col-span` overrides removed
- Touch targets minimum `44px`