"use client"; import React, { useEffect, useRef, useState } from "react"; export interface ScrollStackCard { title: string; subtitle?: string; badge?: string; backgroundImage?: string; content?: React.ReactNode; } interface ScrollStackProps { cards: ScrollStackCard[]; backgroundColor?: string; cardHeight?: string; animationDuration?: string; sectionHeightMultiplier?: number; intersectionThreshold?: number; className?: string; } const defaultBackgrounds = [ "https://images.pexels.com/photos/6985136/pexels-photo-6985136.jpeg", "https://images.pexels.com/photos/6985128/pexels-photo-6985128.jpeg", "https://images.pexels.com/photos/2847648/pexels-photo-2847648.jpeg", ]; const ScrollStack: React.FC = ({ cards, backgroundColor = "bg-background", // Changed default to "bg-background" cardHeight = "60vh", animationDuration = "0.5s", sectionHeightMultiplier = 3, intersectionThreshold = 0.1, className = "", }) => { const scrollableSectionRef = useRef(null); const sectionRef = useRef(null); const cardsContainerRef = useRef(null); const [activeCardIndex, setActiveCardIndex] = useState(0); const [isIntersecting, setIsIntersecting] = useState(false); const ticking = useRef(false); const cardCount = Math.min(cards.length, 5); const cardStyle = { height: cardHeight, maxHeight: "500px", borderRadius: "20px", transition: `transform ${animationDuration} cubic-bezier(0.19, 1, 0.22, 1), opacity ${animationDuration} cubic-bezier(0.19, 1, 0.22, 1)`, willChange: "transform, opacity", }; useEffect(() => { const observer = new IntersectionObserver( (entries) => { const [entry] = entries; setIsIntersecting(entry.isIntersecting); }, { threshold: intersectionThreshold } ); if (sectionRef.current) { observer.observe(sectionRef.current); } const handleScroll = () => { if (!ticking.current) { requestAnimationFrame(() => { if (!sectionRef.current || !cardsContainerRef.current) return; const sectionRect = sectionRef.current.getBoundingClientRect(); const parentRect = scrollableSectionRef.current?.getBoundingClientRect(); const viewportHeight = parentRect?.height ?? window.innerHeight; const sectionTop = sectionRect.top - (parentRect?.top ?? 0); const sectionHeight = sectionRef.current.offsetHeight; const scrollableDistance = sectionHeight - viewportHeight; let progress = 0; if (sectionTop <= 0 && Math.abs(sectionTop) <= scrollableDistance) { progress = Math.abs(sectionTop) / scrollableDistance; } else if (sectionTop <= 0) { progress = 1; } let newActiveIndex = 0; const progressPerCard = 1 / cardCount; for (let i = 0; i < cardCount; i++) { if (progress >= progressPerCard * (i + 1)) { newActiveIndex = i + 1; } } setActiveCardIndex(Math.min(newActiveIndex, cardCount - 1)); ticking.current = false; }); ticking.current = true; } }; const scrollElement = scrollableSectionRef.current; scrollElement?.addEventListener("scroll", handleScroll, { passive: true }); handleScroll(); return () => { scrollElement?.removeEventListener("scroll", handleScroll); if (sectionRef.current) observer.unobserve(sectionRef.current); }; }, [cardCount, sectionHeightMultiplier, intersectionThreshold]); const getCardTransform = (index: number) => { const isVisible = isIntersecting && activeCardIndex >= index; const scale = 0.9 + index * 0.05; let translateY = "100px"; if (isVisible) { translateY = `${90 - index * 30}px`; } return { transform: `translateY(${translateY}) scale(${scale})`, opacity: isVisible ? (index === 0 ? 0.9 : 1) : 0, zIndex: 10 + index * 10, pointerEvents: isVisible ? "auto" : "none", }; }; return (
{cards.slice(0, 5).map((card, index) => { const cardTransform = getCardTransform(index); const backgroundImage = card.backgroundImage || defaultBackgrounds[index % defaultBackgrounds.length]; return (
{card.badge && (
{card.badge}
)}
{card.content ? ( card.content ) : (

{card.title}

{card.subtitle && (

{card.subtitle}

)}
)}
); })}
); }; export default ScrollStack;