UNPKG

reactbits-mcp-server

Version:

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

237 lines (209 loc) 5.86 kB
import { useRef, useState, useEffect } from "react"; import Matter from "matter-js"; const FallingText = ({ text = "", highlightWords = [], trigger = "auto", backgroundColor = "transparent", wireframes = false, gravity = 1, mouseConstraintStiffness = 0.2, fontSize = "1rem", }) => { const containerRef = useRef(null); const textRef = useRef(null); const canvasContainerRef = useRef(null); const [effectStarted, setEffectStarted] = useState(false); useEffect(() => { if (!textRef.current) return; const words = text.split(" "); const newHTML = words .map((word) => { const isHighlighted = highlightWords.some((hw) => word.startsWith(hw)); return `<span class="inline-block mx-[2px] select-none ${isHighlighted ? "text-cyan-500 font-bold" : "" }" > ${word} </span>`; }) .join(" "); textRef.current.innerHTML = newHTML; }, [text, highlightWords]); useEffect(() => { if (trigger === "auto") { setEffectStarted(true); return; } if (trigger === "scroll" && containerRef.current) { const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { setEffectStarted(true); observer.disconnect(); } }, { threshold: 0.1 } ); observer.observe(containerRef.current); return () => observer.disconnect(); } }, [trigger]); useEffect(() => { if (!effectStarted) return; const { Engine, Render, World, Bodies, Runner, Mouse, MouseConstraint, } = Matter; const containerRect = containerRef.current.getBoundingClientRect(); const width = containerRect.width; const height = containerRect.height; if (width <= 0 || height <= 0) return; const engine = Engine.create(); engine.world.gravity.y = gravity; const render = Render.create({ element: canvasContainerRef.current, engine, options: { width, height, background: backgroundColor, wireframes, }, }); const boundaryOptions = { isStatic: true, render: { fillStyle: "transparent" }, }; const floor = Bodies.rectangle( width / 2, height + 25, width, 50, boundaryOptions ); const leftWall = Bodies.rectangle( -25, height / 2, 50, height, boundaryOptions ); const rightWall = Bodies.rectangle( width + 25, height / 2, 50, height, boundaryOptions ); const ceiling = Bodies.rectangle( width / 2, -25, width, 50, boundaryOptions ); const wordSpans = textRef.current.querySelectorAll("span"); const wordBodies = [...wordSpans].map((elem) => { const rect = elem.getBoundingClientRect(); const x = rect.left - containerRect.left + rect.width / 2; const y = rect.top - containerRect.top + rect.height / 2; const body = Bodies.rectangle(x, y, rect.width, rect.height, { render: { fillStyle: "transparent" }, restitution: 0.8, frictionAir: 0.01, friction: 0.2, }); Matter.Body.setVelocity(body, { x: (Math.random() - 0.5) * 5, y: 0, }); Matter.Body.setAngularVelocity(body, (Math.random() - 0.5) * 0.05); return { elem, body }; }); wordBodies.forEach(({ elem, body }) => { elem.style.position = "absolute"; elem.style.left = `${body.position.x - body.bounds.max.x + body.bounds.min.x / 2 }px`; elem.style.top = `${body.position.y - body.bounds.max.y + body.bounds.min.y / 2 }px`; elem.style.transform = "none"; }); const mouse = Mouse.create(containerRef.current); const mouseConstraint = MouseConstraint.create(engine, { mouse, constraint: { stiffness: mouseConstraintStiffness, render: { visible: false }, }, }); render.mouse = mouse; World.add(engine.world, [ floor, leftWall, rightWall, ceiling, mouseConstraint, ...wordBodies.map((wb) => wb.body), ]); const runner = Runner.create(); Runner.run(runner, engine); Render.run(render); const updateLoop = () => { wordBodies.forEach(({ body, elem }) => { const { x, y } = body.position; elem.style.left = `${x}px`; elem.style.top = `${y}px`; elem.style.transform = `translate(-50%, -50%) rotate(${body.angle}rad)`; }); Matter.Engine.update(engine); requestAnimationFrame(updateLoop); }; updateLoop(); return () => { Render.stop(render); Runner.stop(runner); if (render.canvas && canvasContainerRef.current) { // eslint-disable-next-line react-hooks/exhaustive-deps canvasContainerRef.current.removeChild(render.canvas); } World.clear(engine.world); Engine.clear(engine); }; }, [ effectStarted, gravity, wireframes, backgroundColor, mouseConstraintStiffness, ]); const handleTrigger = () => { if (!effectStarted && (trigger === "click" || trigger === "hover")) { setEffectStarted(true); } }; return ( <div ref={containerRef} className="relative z-[1] w-full h-full cursor-pointer text-center pt-8 overflow-hidden" onClick={trigger === "click" ? handleTrigger : undefined} onMouseEnter={trigger === "hover" ? handleTrigger : undefined} > <div ref={textRef} className="inline-block" style={{ fontSize, lineHeight: 1.4, }} /> <div className="absolute top-0 left-0 z-0" ref={canvasContainerRef} /> </div> ); }; export default FallingText;