michaelpeskov/components/PinnedStory.tsx

161 lines
4.5 KiB
TypeScript

'use client'
import { useLayoutEffect, useRef } from 'react'
import { gsap } from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
import { animateWords } from '@/lib/animateSplit'
if (typeof window !== 'undefined') {
gsap.registerPlugin(ScrollTrigger)
}
interface StoryStep {
title: string
copy: string
highlight?: boolean
}
interface PinnedStoryProps {
steps: StoryStep[]
className?: string
}
export default function PinnedStory({ steps, className = '' }: PinnedStoryProps) {
const containerRef = useRef<HTMLDivElement>(null)
const contentRef = useRef<HTMLDivElement>(null)
useLayoutEffect(() => {
const container = containerRef.current
const content = contentRef.current
if (!container || !content) return
// Check for reduced motion
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
const ctx = gsap.context(() => {
if (!prefersReducedMotion) {
// Pin the section
ScrollTrigger.create({
trigger: container,
pin: content,
pinSpacing: true,
start: 'top top',
end: '+=150%',
invalidateOnRefresh: true
})
}
// Animate each step
const stepElements = gsap.utils.toArray<HTMLElement>('.story-step')
stepElements.forEach((step, i) => {
const title = step.querySelector('.story-title') as HTMLElement
const copy = step.querySelector('.story-copy') as HTMLElement
if (prefersReducedMotion) {
// Simple fade for reduced motion
gsap.from(step, {
opacity: 0,
y: 20,
duration: 0.6,
scrollTrigger: {
trigger: step,
start: 'top 80%',
toggleActions: 'play none none reverse'
}
})
} else {
// Full animation
const tl = gsap.timeline({
scrollTrigger: {
trigger: step,
start: 'top 70%',
toggleActions: 'play none none reverse'
}
})
tl.from(step, {
opacity: 0,
scale: 0.95,
duration: 0.8,
ease: 'power2.out'
})
if (title) {
tl.add(() => animateWords(title, { stagger: 0.05 }), '-=0.4')
}
if (copy) {
tl.from(copy, {
opacity: 0,
y: 30,
duration: 0.6,
ease: 'power2.out'
}, '-=0.2')
}
}
})
// Background color transitions
if (!prefersReducedMotion) {
steps.forEach((step, i) => {
if (step.highlight) {
ScrollTrigger.create({
trigger: `.story-step:nth-child(${i + 1})`,
start: 'top 60%',
end: 'bottom 40%',
onEnter: () => {
gsap.to('body', {
backgroundColor: 'rgba(124, 244, 226, 0.05)',
duration: 1,
ease: 'power2.out'
})
},
onLeave: () => {
gsap.to('body', {
backgroundColor: 'transparent',
duration: 1,
ease: 'power2.out'
})
},
onEnterBack: () => {
gsap.to('body', {
backgroundColor: 'rgba(124, 244, 226, 0.05)',
duration: 1,
ease: 'power2.out'
})
},
onLeaveBack: () => {
gsap.to('body', {
backgroundColor: 'transparent',
duration: 1,
ease: 'power2.out'
})
}
})
}
})
}
}, container)
return () => ctx.revert()
}, [steps])
return (
<section ref={containerRef} className={`min-h-[250vh] ${className}`}>
<div ref={contentRef} className="sticky top-0 h-screen flex items-center justify-center">
<div className="max-w-4xl mx-auto px-6 space-y-24">
{steps.map((step, i) => (
<div key={i} className="story-step text-center">
<h2 className="story-title text-4xl md:text-6xl font-bold mb-8 text-white">
{step.title}
</h2>
<p className="story-copy text-lg md:text-xl opacity-80 text-gray-300 max-w-2xl mx-auto leading-relaxed">
{step.copy}
</p>
</div>
))}
</div>
</div>
</section>
)
}