reactbits-mcp-server
Version:
MCP Server for React Bits - Access 99+ React components with animations, backgrounds, and UI elements
167 lines (148 loc) • 4.83 kB
JSX
import { useRef, useEffect } from "react";
import { gsap } from "gsap";
import { Observer } from "gsap/Observer";
gsap.registerPlugin(Observer);
export default function InfiniteScroll({
width = "30rem",
maxHeight = "100%",
negativeMargin = "-0.5em",
items = [],
itemMinHeight = 150,
isTilted = false,
tiltDirection = "left",
autoplay = false,
autoplaySpeed = 0.5,
autoplayDirection = "down",
pauseOnHover = false,
}) {
const wrapperRef = useRef(null);
const containerRef = useRef(null);
const getTiltTransform = () => {
if (!isTilted) return "none";
return tiltDirection === "left"
? "rotateX(20deg) rotateZ(-20deg) skewX(20deg)"
: "rotateX(20deg) rotateZ(20deg) skewX(-20deg)";
};
useEffect(() => {
const container = containerRef.current;
if (!container) return;
if (items.length === 0) return;
const divItems = gsap.utils.toArray(container.children);
if (!divItems.length) return;
const firstItem = divItems[0];
const itemStyle = getComputedStyle(firstItem);
const itemHeight = firstItem.offsetHeight;
const itemMarginTop = parseFloat(itemStyle.marginTop) || 0;
const totalItemHeight = itemHeight + itemMarginTop;
const totalHeight =
itemHeight * items.length + itemMarginTop * (items.length - 1);
const wrapFn = gsap.utils.wrap(-totalHeight, totalHeight);
divItems.forEach((child, i) => {
const y = i * totalItemHeight;
gsap.set(child, { y });
});
const observer = Observer.create({
target: container,
type: "wheel,touch,pointer",
preventDefault: true,
onPress: ({ target }) => {
target.style.cursor = "grabbing";
},
onRelease: ({ target }) => {
target.style.cursor = "grab";
},
onChange: ({ deltaY, isDragging, event }) => {
const d = event.type === "wheel" ? -deltaY : deltaY;
const distance = isDragging ? d * 5 : d * 10;
divItems.forEach((child) => {
gsap.to(child, {
duration: 0.5,
ease: "expo.out",
y: `+=${distance}`,
modifiers: {
y: gsap.utils.unitize(wrapFn),
},
});
});
},
});
let rafId;
if (autoplay) {
const directionFactor = autoplayDirection === "down" ? 1 : -1;
const speedPerFrame = autoplaySpeed * directionFactor;
const tick = () => {
divItems.forEach((child) => {
gsap.set(child, {
y: `+=${speedPerFrame}`,
modifiers: {
y: gsap.utils.unitize(wrapFn),
},
});
});
rafId = requestAnimationFrame(tick);
};
rafId = requestAnimationFrame(tick);
if (pauseOnHover) {
const stopTicker = () => rafId && cancelAnimationFrame(rafId);
const startTicker = () => (rafId = requestAnimationFrame(tick));
container.addEventListener("mouseenter", stopTicker);
container.addEventListener("mouseleave", startTicker);
return () => {
observer.kill();
stopTicker();
container.removeEventListener("mouseenter", stopTicker);
container.removeEventListener("mouseleave", startTicker);
};
} else {
return () => {
observer.kill();
rafId && cancelAnimationFrame(rafId);
};
}
}
return () => {
observer.kill();
if (rafId) cancelAnimationFrame(rafId);
};
}, [
items,
autoplay,
autoplaySpeed,
autoplayDirection,
pauseOnHover,
isTilted,
tiltDirection,
negativeMargin,
]);
return (
<div
className="relative flex items-center justify-center w-full overflow-hidden overscroll-none border-t-2 border-b-2 border-t-dotted border-b-dotted border-transparent"
ref={wrapperRef}
style={{ maxHeight }}
>
<div className="absolute top-0 left-0 w-full h-1/4 bg-gradient-to-b from-black to-transparent z-10 pointer-events-none"></div>
<div className="absolute bottom-0 left-0 w-full h-1/4 bg-gradient-to-t from-black to-transparent z-10 pointer-events-none"></div>
<div
className="flex flex-col overscroll-contain px-4 cursor-grab origin-center"
ref={containerRef}
style={{
width,
transform: getTiltTransform(),
}}
>
{items.map((item, i) => (
<div
className="flex items-center justify-center p-4 text-xl font-semibold text-center border-2 border-white rounded-[15px] select-none box-border relative"
key={i}
style={{
height: `${itemMinHeight}px`,
marginTop: negativeMargin,
}}
>
{item.content}
</div>
))}
</div>
</div>
);
}