lightswind
Version:
A collection of beautifully crafted React Components, Blocks & Templates for Modern Developers. Create stunning web applications effortlessly by using our 160+ professional and animated react components.
148 lines • 9.81 kB
JavaScript
;
"use client";
Object.defineProperty(exports, "__esModule", { value: true });
const jsx_runtime_1 = require("react/jsx-runtime");
const react_1 = require("react");
const defaultBgs = [
"https://images.pexels.com/photos/6985136/pexels-photo-6985136.jpeg?auto=compress&cs=tinysrgb&w=1200",
"https://images.pexels.com/photos/6985128/pexels-photo-6985128.jpeg?auto=compress&cs=tinysrgb&w=1200",
"https://images.pexels.com/photos/2847648/pexels-photo-2847648.jpeg?auto=compress&cs=tinysrgb&w=1200",
"https://images.pexels.com/photos/1103970/pexels-photo-1103970.jpeg?auto=compress&cs=tinysrgb&w=1200",
"https://images.pexels.com/photos/325185/pexels-photo-325185.jpeg?auto=compress&cs=tinysrgb&w=1200",
];
const ScrollStack = ({ cards, cardHeight = 420, scrollPerCard = 300, className = "", }) => {
const containerRef = (0, react_1.useRef)(null);
const stickyPanelRef = (0, react_1.useRef)(null); // NEW: Ref for direct DOM manipulation
const [scrolled, setScrolled] = (0, react_1.useState)(0);
const [vpH, setVpH] = (0, react_1.useState)(600);
// Stable refs — measured once on mount / resize
const scrollParentRef = (0, react_1.useRef)(null);
const containerOffsetRef = (0, react_1.useRef)(0);
const list = cards.slice(0, 5);
const N = list.length;
const totalScrollZone = N * scrollPerCard;
(0, react_1.useEffect)(() => {
const el = containerRef.current;
if (!el)
return;
// ── 1. Find scroll parent ────────────────────────────────────────────────
let scrollParent = null;
let node = el.parentElement;
while (node) {
const s = window.getComputedStyle(node);
if (/auto|scroll/.test(s.overflow) || /auto|scroll/.test(s.overflowY)) {
scrollParent = node;
break;
}
node = node.parentElement;
}
scrollParentRef.current = scrollParent;
// ── 2. Measure stable offset in CONTENT SPACE ───────────────────────────
const measure = () => {
const sp = scrollParentRef.current;
const elRect = el.getBoundingClientRect();
if (sp) {
const spRect = sp.getBoundingClientRect();
containerOffsetRef.current = elRect.top - spRect.top + sp.scrollTop;
setVpH(sp.clientHeight);
}
else {
containerOffsetRef.current = elRect.top + window.scrollY;
setVpH(window.innerHeight);
}
};
// Slight delay ensures the DOM/Preview div is fully rendered before measuring
setTimeout(measure, 100);
// ── 3. Hot scroll handler (Direct DOM + React State) ────────────────────
let animId = null;
const update = () => {
if (animId)
cancelAnimationFrame(animId);
animId = requestAnimationFrame(() => {
const sp = scrollParentRef.current;
const scrollTop = sp ? sp.scrollTop : window.scrollY;
const currentScrolled = Math.max(0, scrollTop - containerOffsetRef.current);
// DIRECT DOM MANIPULATION:
// Moves the panel synchronously with the scrollbar, ensuring zero jitter
// and bypassing any CSS overflow: hidden bugs in parent containers.
if (stickyPanelRef.current) {
const innerTop = Math.min(currentScrolled, totalScrollZone);
stickyPanelRef.current.style.transform = `translateY(${innerTop}px)`;
}
// Update React state strictly for card animations (opacity/scale)
setScrolled(currentScrolled);
});
};
// ── 4. Collect ALL scroll ancestors ─────────────────────────────────────
const targets = [window];
let n = el.parentElement;
while (n) {
const s = window.getComputedStyle(n);
if (/auto|scroll/.test(s.overflow) || /auto|scroll/.test(s.overflowY)) {
targets.push(n);
}
n = n.parentElement;
}
targets.forEach((t) => t.addEventListener("scroll", update, { passive: true }));
window.addEventListener("resize", () => { measure(); update(); }, { passive: true });
update();
return () => {
targets.forEach((t) => t.removeEventListener("scroll", update));
if (animId)
cancelAnimationFrame(animId);
};
}, [totalScrollZone]);
const h = vpH || 600;
return ((0, jsx_runtime_1.jsx)("div", { ref: containerRef, className: `relative w-full ${className}`, style: { height: h + totalScrollZone }, children: (0, jsx_runtime_1.jsxs)("div", { ref: stickyPanelRef, style: {
position: "absolute",
top: 0,
left: 0,
right: 0,
height: h,
display: "flex",
alignItems: "center",
justifyContent: "center",
willChange: "transform", // Hardware acceleration for buttery smoothness
}, children: [(0, jsx_runtime_1.jsx)("div", { style: {
position: "relative",
width: "100%",
maxWidth: "56rem",
margin: "0 auto",
padding: "0 1rem",
height: cardHeight,
}, children: list.map((card, index) => {
const bg = card.backgroundImage ?? defaultBgs[index % defaultBgs.length];
const revealAt = index * scrollPerCard;
const isVisible = scrolled >= revealAt;
const above = Math.max(0, Math.min(N - 1 - index, Math.floor((scrolled - revealAt) / scrollPerCard)));
const entryProgress = isVisible ? Math.min(1, (scrolled - revealAt) / 80) : 0;
const entryY = (1 - entryProgress) * 80;
const pushY = above * 12;
const scale = 1 - above * 0.03;
const opacity = isVisible ? Math.max(0.6, 1 - above * 0.1) : 0;
return ((0, jsx_runtime_1.jsxs)("div", { className: "absolute inset-x-0 overflow-hidden rounded-2xl shadow-2xl", style: {
height: cardHeight,
top: 0,
zIndex: 10 + index,
transform: `translateY(${entryY - pushY}px) scale(${scale})`,
opacity,
transition: "transform 0.55s cubic-bezier(0.22,1,0.36,1), opacity 0.45s ease",
willChange: "transform, opacity",
transformOrigin: "center top",
}, children: [(0, jsx_runtime_1.jsx)("div", { className: "absolute inset-0", style: {
backgroundImage: `url('${bg}')`,
backgroundSize: "cover",
backgroundPosition: "center",
} }), (0, jsx_runtime_1.jsx)("div", { className: "absolute inset-0 bg-gradient-to-t from-black/85 via-black/25 to-transparent" }), card.badge && ((0, jsx_runtime_1.jsx)("div", { className: "absolute top-5 right-5 z-10", children: (0, jsx_runtime_1.jsx)("span", { className: "px-4 py-1.5 rounded-full bg-white/20 backdrop-blur-md text-white text-sm font-medium border border-white/30", children: card.badge }) })), (0, jsx_runtime_1.jsx)("div", { className: "absolute inset-0 flex items-end p-6 sm:p-10 z-10", children: card.content ?? ((0, jsx_runtime_1.jsxs)("div", { className: "max-w-lg", children: [(0, jsx_runtime_1.jsx)("h3", { className: "text-2xl sm:text-3xl font-bold text-white mb-2 leading-tight", children: card.title }), card.subtitle && ((0, jsx_runtime_1.jsx)("p", { className: "text-white/70 text-sm sm:text-base leading-relaxed line-clamp-3", children: card.subtitle }))] })) }), (0, jsx_runtime_1.jsxs)("div", { className: "absolute bottom-5 right-6 z-10 text-white/40 text-xs font-mono tracking-widest", children: [String(index + 1).padStart(2, "0"), " / ", String(N).padStart(2, "0")] })] }, index));
}) }), (0, jsx_runtime_1.jsx)("div", { className: "absolute right-5 top-1/2 -translate-y-1/2 flex flex-col gap-2.5 z-50", children: list.map((_, i) => {
const active = scrolled >= i * scrollPerCard;
return ((0, jsx_runtime_1.jsx)("div", { className: "rounded-full transition-all duration-300", style: {
width: active ? 8 : 5,
height: active ? 8 : 5,
background: active ? "rgba(255,255,255,0.9)" : "rgba(120,120,120,0.4)",
boxShadow: active ? "0 0 6px rgba(255,255,255,0.5)" : "none",
} }, i));
}) }), scrolled < 20 && ((0, jsx_runtime_1.jsx)("div", { className: "absolute bottom-8 inset-x-0 flex justify-center z-50 pointer-events-none", children: (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-white/40 tracking-[0.2em] uppercase animate-bounce", children: "scroll to explore" }) }))] }) }));
};
exports.default = ScrollStack;
//# sourceMappingURL=scroll-stack.js.map