MVp
This commit is contained in:
161
components/PinnedStory.tsx
Normal file
161
components/PinnedStory.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user