UNPKG

reactbits-mcp-server

Version:

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

212 lines (187 loc) 5.77 kB
import { useRef, useState, useEffect } from "react"; import Matter from "matter-js"; import "./FallingText.css"; const FallingText = ({ className = '', text = '', highlightWords = [], highlightClass = "highlighted", 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="word ${isHighlighted ? highlightClass : ""}">${word}</span>`; }) .join(" "); textRef.current.innerHTML = newHTML; }, [text, highlightWords, highlightClass]); 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(".word"); 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={`falling-text-container ${className}`} onClick={trigger === "click" ? handleTrigger : undefined} onMouseEnter={trigger === "hover" ? handleTrigger : undefined} style={{ position: "relative", overflow: "hidden", }} > <div ref={textRef} className="falling-text-target" style={{ fontSize: fontSize, lineHeight: 1.4, }} /> <div ref={canvasContainerRef} className="falling-text-canvas" /> </div> ); }; export default FallingText;