{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "FallingText-JS-CSS",
	"title": "FallingText",
	"description": "Characters fall with gravity + bounce creating a playful entrance.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:file",
			"path": "FallingText.css",
			"target": "@components/FallingText.css",
			"content": ".falling-text-container {\n  position: relative;\n  z-index: 1;\n  width: 100%;\n  height: 100%;\n  cursor: pointer;\n  text-align: center;\n  padding-top: 2em;\n}\n\n.falling-text-target {\n  display: inline-block;\n}\n\n.word {\n  display: inline-block;\n  margin: 0 2px;\n  user-select: none;\n}\n\n.highlighted {\n  color: cyan;\n  font-weight: bold;\n}\n\n.falling-text-canvas {\n  position: absolute;\n  top: 0;\n  left: 0;\n  z-index: 0;\n}\n"
		},
		{
			"type": "registry:component",
			"path": "FallingText.jsx",
			"content": "import { useRef, useState, useEffect } from 'react';\nimport Matter from 'matter-js';\nimport './FallingText.css';\n\nconst FallingText = ({\n  className = '',\n  text = '',\n  highlightWords = [],\n  highlightClass = 'highlighted',\n  trigger = 'auto',\n  backgroundColor = 'transparent',\n  wireframes = false,\n  gravity = 1,\n  mouseConstraintStiffness = 0.2,\n  fontSize = '1rem'\n}) => {\n  const containerRef = useRef(null);\n  const textRef = useRef(null);\n  const canvasContainerRef = useRef(null);\n\n  const [effectStarted, setEffectStarted] = useState(false);\n\n  useEffect(() => {\n    if (!textRef.current) return;\n    const words = text.split(' ');\n    const newHTML = words\n      .map(word => {\n        const isHighlighted = highlightWords.some(hw => word.startsWith(hw));\n        return `<span class=\"word ${isHighlighted ? highlightClass : ''}\">${word}</span>`;\n      })\n      .join(' ');\n    textRef.current.innerHTML = newHTML;\n  }, [text, highlightWords, highlightClass]);\n\n  useEffect(() => {\n    if (trigger === 'auto') {\n      setEffectStarted(true);\n      return;\n    }\n    if (trigger === 'scroll' && containerRef.current) {\n      const observer = new IntersectionObserver(\n        ([entry]) => {\n          if (entry.isIntersecting) {\n            setEffectStarted(true);\n            observer.disconnect();\n          }\n        },\n        { threshold: 0.1 }\n      );\n      observer.observe(containerRef.current);\n      return () => observer.disconnect();\n    }\n  }, [trigger]);\n\n  useEffect(() => {\n    if (!effectStarted) return;\n\n    const { Engine, Render, World, Bodies, Runner, Mouse, MouseConstraint } = Matter;\n\n    const containerRect = containerRef.current.getBoundingClientRect();\n    const width = containerRect.width;\n    const height = containerRect.height;\n\n    if (width <= 0 || height <= 0) {\n      return;\n    }\n\n    const engine = Engine.create();\n    engine.world.gravity.y = gravity;\n\n    const render = Render.create({\n      element: canvasContainerRef.current,\n      engine,\n      options: {\n        width,\n        height,\n        background: backgroundColor,\n        wireframes\n      }\n    });\n\n    const boundaryOptions = {\n      isStatic: true,\n      render: { fillStyle: 'transparent' }\n    };\n    const floor = Bodies.rectangle(width / 2, height + 25, width, 50, boundaryOptions);\n    const leftWall = Bodies.rectangle(-25, height / 2, 50, height, boundaryOptions);\n    const rightWall = Bodies.rectangle(width + 25, height / 2, 50, height, boundaryOptions);\n    const ceiling = Bodies.rectangle(width / 2, -25, width, 50, boundaryOptions);\n\n    const wordSpans = textRef.current.querySelectorAll('.word');\n    const wordBodies = [...wordSpans].map(elem => {\n      const rect = elem.getBoundingClientRect();\n\n      const x = rect.left - containerRect.left + rect.width / 2;\n      const y = rect.top - containerRect.top + rect.height / 2;\n\n      const body = Bodies.rectangle(x, y, rect.width, rect.height, {\n        render: { fillStyle: 'transparent' },\n        restitution: 0.8,\n        frictionAir: 0.01,\n        friction: 0.2\n      });\n\n      Matter.Body.setVelocity(body, {\n        x: (Math.random() - 0.5) * 5,\n        y: 0\n      });\n      Matter.Body.setAngularVelocity(body, (Math.random() - 0.5) * 0.05);\n      return { elem, body };\n    });\n\n    wordBodies.forEach(({ elem, body }) => {\n      elem.style.position = 'absolute';\n      elem.style.left = `${body.position.x - body.bounds.max.x + body.bounds.min.x / 2}px`;\n      elem.style.top = `${body.position.y - body.bounds.max.y + body.bounds.min.y / 2}px`;\n      elem.style.transform = 'none';\n    });\n\n    const mouse = Mouse.create(containerRef.current);\n    const mouseConstraint = MouseConstraint.create(engine, {\n      mouse,\n      constraint: {\n        stiffness: mouseConstraintStiffness,\n        render: { visible: false }\n      }\n    });\n    render.mouse = mouse;\n\n    World.add(engine.world, [floor, leftWall, rightWall, ceiling, mouseConstraint, ...wordBodies.map(wb => wb.body)]);\n\n    const runner = Runner.create();\n    Runner.run(runner, engine);\n    Render.run(render);\n\n    const updateLoop = () => {\n      wordBodies.forEach(({ body, elem }) => {\n        const { x, y } = body.position;\n        elem.style.left = `${x}px`;\n        elem.style.top = `${y}px`;\n        elem.style.transform = `translate(-50%, -50%) rotate(${body.angle}rad)`;\n      });\n      Matter.Engine.update(engine);\n      requestAnimationFrame(updateLoop);\n    };\n    updateLoop();\n\n    return () => {\n      Render.stop(render);\n      Runner.stop(runner);\n      if (render.canvas && canvasContainerRef.current) {\n        // eslint-disable-next-line react-hooks/exhaustive-deps\n        canvasContainerRef.current.removeChild(render.canvas);\n      }\n      World.clear(engine.world);\n      Engine.clear(engine);\n    };\n  }, [effectStarted, gravity, wireframes, backgroundColor, mouseConstraintStiffness]);\n\n  const handleTrigger = () => {\n    if (!effectStarted && (trigger === 'click' || trigger === 'hover')) {\n      setEffectStarted(true);\n    }\n  };\n\n  return (\n    <div\n      ref={containerRef}\n      className={`falling-text-container ${className}`}\n      onClick={trigger === 'click' ? handleTrigger : undefined}\n      onMouseEnter={trigger === 'hover' ? handleTrigger : undefined}\n      style={{\n        position: 'relative',\n        overflow: 'hidden'\n      }}\n    >\n      <div\n        ref={textRef}\n        className=\"falling-text-target\"\n        style={{\n          fontSize: fontSize,\n          lineHeight: 1.4\n        }}\n      />\n      <div ref={canvasContainerRef} className=\"falling-text-canvas\" />\n    </div>\n  );\n};\n\nexport default FallingText;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"matter-js@^0.20.0"
	]
}