UNPKG

reactbits-mcp-server

Version:

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

240 lines (213 loc) 6.42 kB
import { useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { gsap } from "gsap"; const useMedia = ( queries, values, defaultValue ) => { const get = () => values[queries.findIndex((q) => matchMedia(q).matches)] ?? defaultValue; const [value, setValue] = useState(get); useEffect(() => { const handler = () => setValue(get); queries.forEach((q) => matchMedia(q).addEventListener("change", handler)); return () => queries.forEach((q) => matchMedia(q).removeEventListener("change", handler) ); // eslint-disable-next-line react-hooks/exhaustive-deps }, [queries]); return value; }; const useMeasure = () => { const ref = useRef(null); const [size, setSize] = useState({ width: 0, height: 0 }); useLayoutEffect(() => { if (!ref.current) return; const ro = new ResizeObserver(([entry]) => { const { width, height } = entry.contentRect; setSize({ width, height }); }); ro.observe(ref.current); return () => ro.disconnect(); }, []); return [ref, size]; }; const preloadImages = async (urls) => { await Promise.all( urls.map( (src) => new Promise((resolve) => { const img = new Image(); img.src = src; img.onload = img.onerror = () => resolve(); }) ) ); }; const Masonry = ({ items, ease = "power3.out", duration = 0.6, stagger = 0.05, animateFrom = "bottom", scaleOnHover = true, hoverScale = 0.95, blurToFocus = true, colorShiftOnHover = false, }) => { const columns = useMedia( [ "(min-width:1500px)", "(min-width:1000px)", "(min-width:600px)", "(min-width:400px)", ], [5, 4, 3, 2], 1 ); const [containerRef, { width }] = useMeasure(); const [imagesReady, setImagesReady] = useState(false); const getInitialPosition = (item) => { const containerRect = containerRef.current?.getBoundingClientRect(); if (!containerRect) return { x: item.x, y: item.y }; let direction = animateFrom; if (animateFrom === "random") { const dirs = ["top", "bottom", "left", "right"]; direction = dirs[ Math.floor(Math.random() * dirs.length) ]; } switch (direction) { case "top": return { x: item.x, y: -200 }; case "bottom": return { x: item.x, y: window.innerHeight + 200 }; case "left": return { x: -200, y: item.y }; case "right": return { x: window.innerWidth + 200, y: item.y }; case "center": return { x: containerRect.width / 2 - item.w / 2, y: containerRect.height / 2 - item.h / 2, }; default: return { x: item.x, y: item.y + 100 }; } }; useEffect(() => { preloadImages(items.map((i) => i.img)).then(() => setImagesReady(true)); }, [items]); const grid = useMemo(() => { if (!width) return []; const colHeights = new Array(columns).fill(0); const gap = 16; const totalGaps = (columns - 1) * gap; const columnWidth = (width - totalGaps) / columns; return items.map((child) => { const col = colHeights.indexOf(Math.min(...colHeights)); const x = col * (columnWidth + gap); const height = child.height / 2; const y = colHeights[col]; colHeights[col] += height + gap; return { ...child, x, y, w: columnWidth, h: height }; }); }, [columns, items, width]); const hasMounted = useRef(false); useLayoutEffect(() => { if (!imagesReady) return; grid.forEach((item, index) => { const selector = `[data-key="${item.id}"]`; const animProps = { x: item.x, y: item.y, width: item.w, height: item.h }; if (!hasMounted.current) { const start = getInitialPosition(item); gsap.fromTo( selector, { opacity: 0, x: start.x, y: start.y, width: item.w, height: item.h, ...(blurToFocus && { filter: "blur(10px)" }), }, { opacity: 1, ...animProps, ...(blurToFocus && { filter: "blur(0px)" }), duration: 0.8, ease: "power3.out", delay: index * stagger, } ); } else { gsap.to(selector, { ...animProps, duration, ease, overwrite: "auto", }); } }); hasMounted.current = true; // eslint-disable-next-line react-hooks/exhaustive-deps }, [grid, imagesReady, stagger, animateFrom, blurToFocus, duration, ease]); const handleMouseEnter = (id, element) => { if (scaleOnHover) { gsap.to(`[data-key="${id}"]`, { scale: hoverScale, duration: 0.3, ease: "power2.out" }); } if (colorShiftOnHover) { const overlay = element.querySelector(".color-overlay"); if (overlay) gsap.to(overlay, { opacity: 0.3, duration: 0.3 }); } }; const handleMouseLeave = (id, element) => { if (scaleOnHover) { gsap.to(`[data-key="${id}"]`, { scale: 1, duration: 0.3, ease: "power2.out" }); } if (colorShiftOnHover) { const overlay = element.querySelector(".color-overlay"); if (overlay) gsap.to(overlay, { opacity: 0, duration: 0.3 }); } }; return ( <div ref={containerRef} className="relative w-full h-full"> {grid.map((item) => ( <div key={item.id} data-key={item.id} className="absolute box-content" style={{ willChange: "transform, width, height, opacity" }} onClick={() => window.open(item.url, "_blank", "noopener")} onMouseEnter={(e) => handleMouseEnter(item.id, e.currentTarget)} onMouseLeave={(e) => handleMouseLeave(item.id, e.currentTarget)} > <div className="relative w-full h-full bg-cover bg-center rounded-[10px] shadow-[0px_10px_50px_-10px_rgba(0,0,0,0.2)] uppercase text-[10px] leading-[10px]" style={{ backgroundImage: `url(${item.img})` }} > {colorShiftOnHover && ( <div className="color-overlay absolute inset-0 rounded-[10px] bg-gradient-to-tr from-pink-500/50 to-sky-500/50 opacity-0 pointer-events-none" /> )} </div> </div> ))} </div> ); }; export default Masonry;