File size: 4,560 Bytes
86fbe37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
'use client';
import { useRef, useEffect, useCallback } from 'react';
import './ScrollStack.css';

export const ScrollStackItem = ({ children, itemClassName = '' }) => (
    <div className={`scroll-stack-card ${itemClassName}`.trim()}>{children}</div>
);

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 (
        <div className={`scroll-stack-container ${className}`.trim()} ref={containerRef}>
            {children}
            <div className="scroll-stack-end" />
        </div>
    );
};

export default ScrollStack;