'use client'; import { useRef, useEffect, useCallback } from 'react'; import './ScrollStack.css'; export const ScrollStackItem = ({ children, itemClassName = '' }) => (
{children}
); const ScrollStack = ({ children, className = '', itemDistance = 100, itemScale = 0.03, itemStackDistance = 30, baseScale = 0.85, onStackComplete }) => { const containerRef = useRef(null); const cardsRef = useRef([]); const animationRef = useRef(null); const currentScalesRef = useRef([]); const targetScalesRef = useRef([]); // Faster lerp for more responsive feel const lerp = useCallback((start, end, factor) => { const diff = end - start; if (Math.abs(diff) < 0.001) return end; return start + diff * factor; }, []); useEffect(() => { const container = containerRef.current; if (!container) return; const cards = Array.from(container.querySelectorAll('.scroll-stack-card')); cardsRef.current = cards; // Initialize scales currentScalesRef.current = cards.map(() => 1); targetScalesRef.current = cards.map(() => 1); // Mobile detection const isMobile = window.innerWidth < 768; const stickyTopBase = isMobile ? 70 : 100; const stackDistance = isMobile ? 20 : itemStackDistance; const cardMargin = isMobile ? 80 : itemDistance; // Set sticky positions cards.forEach((card, i) => { card.style.position = 'sticky'; card.style.top = `${stickyTopBase + i * stackDistance}px`; card.style.marginBottom = i < cards.length - 1 ? `${cardMargin}px` : '0'; card.style.zIndex = i + 1; }); // Animation settings const lerpFactor = 0.18; // Higher = more responsive let isRunning = true; const animate = () => { if (!isRunning) return; cards.forEach((card, i) => { const rect = card.getBoundingClientRect(); const stickyTop = stickyTopBase + i * stackDistance; const isStuck = rect.top <= stickyTop + 5; // Calculate target scale let targetScale = 1; if (isStuck) { let stackedAbove = 0; for (let j = i + 1; j < cards.length; j++) { const nextRect = cards[j].getBoundingClientRect(); const nextStickyTop = stickyTopBase + j * stackDistance; if (nextRect.top <= nextStickyTop + 5) { stackedAbove++; } } targetScale = Math.max(baseScale, 1 - (stackedAbove * itemScale)); } targetScalesRef.current[i] = targetScale; // Smooth interpolation const currentScale = currentScalesRef.current[i]; const newScale = lerp(currentScale, targetScale, lerpFactor); currentScalesRef.current[i] = newScale; // Apply scale transform only (no blur for performance) card.style.transform = `scale(${newScale.toFixed(4)})`; }); animationRef.current = requestAnimationFrame(animate); }; // Start loop animationRef.current = requestAnimationFrame(animate); // Scroll listener for onStackComplete const handleScroll = () => { const lastCard = cards[cards.length - 1]; if (lastCard) { const rect = lastCard.getBoundingClientRect(); const stickyTop = 100 + (cards.length - 1) * itemStackDistance; if (rect.top <= stickyTop + 5) { onStackComplete?.(); } } }; window.addEventListener('scroll', handleScroll, { passive: true }); return () => { isRunning = false; if (animationRef.current) { cancelAnimationFrame(animationRef.current); } window.removeEventListener('scroll', handleScroll); }; }, [itemDistance, itemScale, itemStackDistance, baseScale, lerp, onStackComplete]); return (
{children}
); }; export default ScrollStack;