{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "FallingText-TS-TW",
	"title": "FallingText",
	"description": "Characters fall with gravity + bounce creating a playful entrance.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "FallingText/FallingText.tsx",
			"content": "import { useRef, useState, useEffect } from 'react';\nimport Matter from 'matter-js';\n\ninterface FallingTextProps {\n  text?: string;\n  highlightWords?: string[];\n  trigger?: 'auto' | 'scroll' | 'click' | 'hover';\n  backgroundColor?: string;\n  wireframes?: boolean;\n  gravity?: number;\n  mouseConstraintStiffness?: number;\n  fontSize?: string;\n}\n\nconst FallingText: React.FC<FallingTextProps> = ({\n  text = '',\n  highlightWords = [],\n  trigger = 'auto',\n  backgroundColor = 'transparent',\n  wireframes = false,\n  gravity = 1,\n  mouseConstraintStiffness = 0.2,\n  fontSize = '1rem'\n}) => {\n  const containerRef = useRef<HTMLDivElement | null>(null);\n  const textRef = useRef<HTMLDivElement | null>(null);\n  const canvasContainerRef = useRef<HTMLDivElement | null>(null);\n\n  const [effectStarted, setEffectStarted] = useState(false);\n\n  useEffect(() => {\n    if (!textRef.current) return;\n    const words = text.split(' ');\n\n    const newHTML = words\n      .map(word => {\n        const isHighlighted = highlightWords.some(hw => word.startsWith(hw));\n        return `<span\n          class=\"inline-block mx-[2px] select-none ${isHighlighted ? 'text-cyan-500 font-bold' : ''}\"\n        >\n          ${word}\n        </span>`;\n      })\n      .join(' ');\n\n    textRef.current.innerHTML = newHTML;\n  }, [text, highlightWords]);\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    if (!containerRef.current || !canvasContainerRef.current) return;\n\n    const containerRect = containerRef.current.getBoundingClientRect();\n    const width = containerRect.width;\n    const height = containerRect.height;\n\n    if (width <= 0 || height <= 0) return;\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    if (!textRef.current) return;\n    const wordSpans = textRef.current.querySelectorAll('span');\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      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\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        canvasContainerRef.current.removeChild(render.canvas);\n      }\n      World.clear(engine.world, false);\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=\"relative z-[1] w-full h-full cursor-pointer text-center pt-8 overflow-hidden\"\n      onClick={trigger === 'click' ? handleTrigger : undefined}\n      onMouseEnter={trigger === 'hover' ? handleTrigger : undefined}\n    >\n      <div\n        ref={textRef}\n        className=\"inline-block\"\n        style={{\n          fontSize,\n          lineHeight: 1.4\n        }}\n      />\n\n      <div className=\"absolute top-0 left-0 z-0\" ref={canvasContainerRef} />\n    </div>\n  );\n};\n\nexport default FallingText;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"matter-js@^0.20.0"
	]
}