UNPKG

reactbits-mcp-server

Version:

MCP Server for React Bits - Access 99+ React components with animations, backgrounds, and UI elements

219 lines (199 loc) 5.35 kB
import React, { Children, cloneElement, forwardRef, isValidElement, useEffect, useMemo, useRef, } from "react"; import gsap from "gsap"; export const Card = forwardRef( ({ customClass, ...rest }, ref) => ( <div ref={ref} {...rest} className={`absolute top-1/2 left-1/2 rounded-xl border border-white bg-black [transform-style:preserve-3d] [will-change:transform] [backface-visibility:hidden] ${customClass ?? ""} ${rest.className ?? ""}`.trim()} /> ) ); Card.displayName = "Card"; const makeSlot = ( i, distX, distY, total ) => ({ x: i * distX, y: -i * distY, z: -i * distX * 1.5, zIndex: total - i, }); const placeNow = (el, slot, skew) => gsap.set(el, { x: slot.x, y: slot.y, z: slot.z, xPercent: -50, yPercent: -50, skewY: skew, transformOrigin: "center center", zIndex: slot.zIndex, force3D: true, }); const CardSwap = ({ width = 500, height = 400, cardDistance = 60, verticalDistance = 70, delay = 5000, pauseOnHover = false, onCardClick, skewAmount = 6, easing = "elastic", children, }) => { const config = easing === "elastic" ? { ease: "elastic.out(0.6,0.9)", durDrop: 2, durMove: 2, durReturn: 2, promoteOverlap: 0.9, returnDelay: 0.05, } : { ease: "power1.inOut", durDrop: 0.8, durMove: 0.8, durReturn: 0.8, promoteOverlap: 0.45, returnDelay: 0.2, }; const childArr = useMemo( () => Children.toArray(children), [children] ); const refs = useMemo( () => childArr.map(() => React.createRef()), // eslint-disable-next-line react-hooks/exhaustive-deps [childArr.length] ); const order = useRef( Array.from({ length: childArr.length }, (_, i) => i) ); const tlRef = useRef(null); const intervalRef = useRef(); const container = useRef(null); useEffect(() => { const total = refs.length; refs.forEach((r, i) => placeNow( r.current, makeSlot(i, cardDistance, verticalDistance, total), skewAmount ) ); const swap = () => { if (order.current.length < 2) return; const [front, ...rest] = order.current; const elFront = refs[front].current; const tl = gsap.timeline(); tlRef.current = tl; tl.to(elFront, { y: "+=500", duration: config.durDrop, ease: config.ease, }); tl.addLabel("promote", `-=${config.durDrop * config.promoteOverlap}`); rest.forEach((idx, i) => { const el = refs[idx].current; const slot = makeSlot(i, cardDistance, verticalDistance, refs.length); tl.set(el, { zIndex: slot.zIndex }, "promote"); tl.to( el, { x: slot.x, y: slot.y, z: slot.z, duration: config.durMove, ease: config.ease, }, `promote+=${i * 0.15}` ); }); const backSlot = makeSlot( refs.length - 1, cardDistance, verticalDistance, refs.length ); tl.addLabel("return", `promote+=${config.durMove * config.returnDelay}`); tl.call( () => { gsap.set(elFront, { zIndex: backSlot.zIndex }); }, undefined, "return" ); tl.set(elFront, { x: backSlot.x, z: backSlot.z }, "return"); tl.to( elFront, { y: backSlot.y, duration: config.durReturn, ease: config.ease, }, "return" ); tl.call(() => { order.current = [...rest, front]; }); }; swap(); intervalRef.current = window.setInterval(swap, delay); if (pauseOnHover) { const node = container.current; const pause = () => { tlRef.current?.pause(); clearInterval(intervalRef.current); }; const resume = () => { tlRef.current?.play(); intervalRef.current = window.setInterval(swap, delay); }; node.addEventListener("mouseenter", pause); node.addEventListener("mouseleave", resume); return () => { node.removeEventListener("mouseenter", pause); node.removeEventListener("mouseleave", resume); clearInterval(intervalRef.current); }; } return () => clearInterval(intervalRef.current); // eslint-disable-next-line react-hooks/exhaustive-deps }, [cardDistance, verticalDistance, delay, pauseOnHover, skewAmount, easing]); const rendered = childArr.map((child, i) => isValidElement(child) ? cloneElement(child, { key: i, ref: refs[i], style: { width, height, ...(child.props.style ?? {}) }, onClick: (e) => { child.props.onClick?.(e); onCardClick?.(i); }, }) : child ); return ( <div ref={container} className="absolute bottom-0 right-0 transform translate-x-[5%] translate-y-[20%] origin-bottom-right perspective-[900px] overflow-visible max-[768px]:translate-x-[25%] max-[768px]:translate-y-[25%] max-[768px]:scale-[0.75] max-[480px]:translate-x-[25%] max-[480px]:translate-y-[25%] max-[480px]:scale-[0.55]" style={{ width, height }} > {rendered} </div> ); }; export default CardSwap;