161 lines
4.5 KiB
TypeScript
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>
|
|
)
|
|
} |