{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "gravity",
  "title": "Gravity",
  "description": "A physics playground where elements fall, pile, collide, and can be dragged and flung.",
  "dependencies": ["matter-js"],
  "devDependencies": ["@types/matter-js"],
  "registryDependencies": ["@godui/godui-theme"],
  "files": [
    {
      "path": "packages/components/src/gravity/gravity.tsx",
      "content": "\"use client\";\n\nimport Matter from \"matter-js\";\nimport * as React from \"react\";\n\ntype Vec2 = { x: number; y: number };\n\ntype RegisteredBody = {\n  element: HTMLElement;\n  body: Matter.Body;\n  isDraggable: boolean;\n};\n\ntype GravityContextValue = {\n  register: (\n    id: string,\n    element: HTMLElement,\n    config: {\n      x: number | string;\n      y: number | string;\n      angle: number;\n      bodyType: \"rectangle\" | \"circle\";\n      isDraggable: boolean;\n      options?: Matter.IChamferableBodyDefinition;\n    },\n  ) => void;\n  unregister: (id: string) => void;\n  reduced: boolean;\n};\n\nconst GravityContext = React.createContext<GravityContextValue | null>(null);\n\nexport type GravityProps = React.HTMLAttributes<HTMLDivElement> & {\n  /** World gravity vector. `y: 1` falls down; `{ x: 0, y: 0 }` floats. */\n  gravity?: Vec2;\n  /** Start the simulation immediately (default) or wait for the API. */\n  autoStart?: boolean;\n  /** Close the top so flung bodies can't escape upward. */\n  addTopWall?: boolean;\n  /** Bounciness of the walls and bodies, 0–1. */\n  restitution?: number;\n};\n\n/** Resolve a percentage/px position string against a container dimension. */\nfunction resolve(value: number | string, size: number): number {\n  if (typeof value === \"number\") return value;\n  if (value.endsWith(\"%\")) return (parseFloat(value) / 100) * size;\n  return parseFloat(value);\n}\n\nconst Gravity = React.forwardRef<HTMLDivElement, GravityProps>(\n  (\n    {\n      gravity = { x: 0, y: 1 },\n      autoStart = true,\n      addTopWall = true,\n      restitution = 0.4,\n      className,\n      children,\n      ...props\n    },\n    forwardedRef,\n  ) => {\n    const reduced = useReducedMotionSafe();\n    const canvasRef = React.useRef<HTMLDivElement>(null);\n    React.useImperativeHandle(\n      forwardedRef,\n      () => canvasRef.current as HTMLDivElement,\n    );\n\n    // Create the engine at render, not in an effect: child `MatterBody` effects\n    // run before the parent's effect, so the engine must already exist when they\n    // register their bodies.\n    const [engine] = React.useState(() => Matter.Engine.create());\n    const bodiesRef = React.useRef(new Map<string, RegisteredBody>());\n    const wallsRef = React.useRef<Matter.Body[]>([]);\n\n    // Keep gravity in sync without rebuilding the world (which would drop the\n    // bodies the children registered).\n    React.useEffect(() => {\n      engine.gravity.x = gravity.x;\n      engine.gravity.y = gravity.y;\n    }, [engine, gravity.x, gravity.y]);\n\n    // Walls, mouse dragging, runner, and the DOM sync loop.\n    React.useEffect(() => {\n      const canvas = canvasRef.current;\n      if (!canvas || reduced) return;\n\n      let width = canvas.getBoundingClientRect().width;\n      let height = canvas.getBoundingClientRect().height;\n\n      const buildWalls = () => {\n        Matter.World.remove(engine.world, wallsRef.current);\n        const t = 200; // thick walls keep fast bodies contained\n        const opts: Matter.IChamferableBodyDefinition = {\n          isStatic: true,\n          restitution,\n        };\n        const walls = [\n          Matter.Bodies.rectangle(\n            width / 2,\n            height + t / 2,\n            width + t * 2,\n            t,\n            opts,\n          ),\n          Matter.Bodies.rectangle(-t / 2, height / 2, t, height + t * 2, opts),\n          Matter.Bodies.rectangle(\n            width + t / 2,\n            height / 2,\n            t,\n            height + t * 2,\n            opts,\n          ),\n        ];\n        if (addTopWall) {\n          walls.push(\n            Matter.Bodies.rectangle(width / 2, -t / 2, width + t * 2, t, opts),\n          );\n        }\n        wallsRef.current = walls;\n        Matter.World.add(engine.world, walls);\n      };\n      buildWalls();\n\n      const mouse = Matter.Mouse.create(canvas);\n      const mouseConstraint = Matter.MouseConstraint.create(engine, {\n        mouse,\n        constraint: { stiffness: 0.2, render: { visible: false } },\n      });\n      // Only draggable bodies respond to the pointer.\n      Matter.Events.on(mouseConstraint, \"startdrag\", (e) => {\n        const dragged = [...bodiesRef.current.values()].find(\n          (b) => b.body === (e as unknown as { body: Matter.Body }).body,\n        );\n        if (dragged && !dragged.isDraggable) {\n          mouseConstraint.constraint.bodyB = null;\n        }\n      });\n      Matter.World.add(engine.world, mouseConstraint);\n\n      const runner = Matter.Runner.create();\n\n      // Sync DOM transforms to body positions every frame.\n      let syncFrame = 0;\n      const sync = () => {\n        for (const { element, body } of bodiesRef.current.values()) {\n          const w = element.offsetWidth;\n          const h = element.offsetHeight;\n          element.style.transform = `translate(${body.position.x - w / 2}px, ${\n            body.position.y - h / 2\n          }px) rotate(${body.angle}rad)`;\n        }\n        syncFrame = requestAnimationFrame(sync);\n      };\n\n      // Pause the physics engine AND the DOM-sync loop whenever the scene is off\n      // screen or the tab is hidden — a physics sim must not burn CPU unseen.\n      let visible = true;\n      let runnerActive = false;\n      const resume = () => {\n        if (autoStart && !runnerActive) {\n          Matter.Runner.run(runner, engine);\n          runnerActive = true;\n        }\n        if (!syncFrame) syncFrame = requestAnimationFrame(sync);\n      };\n      const pause = () => {\n        if (runnerActive) {\n          Matter.Runner.stop(runner);\n          runnerActive = false;\n        }\n        if (syncFrame) {\n          cancelAnimationFrame(syncFrame);\n          syncFrame = 0;\n        }\n      };\n\n      const ro = new ResizeObserver(() => {\n        const r = canvas.getBoundingClientRect();\n        width = r.width;\n        height = r.height;\n        buildWalls();\n      });\n      ro.observe(canvas);\n\n      const io = new IntersectionObserver(\n        ([entry]) => {\n          visible = entry.isIntersecting;\n          if (visible && !document.hidden) resume();\n          else pause();\n        },\n        { threshold: 0 },\n      );\n      io.observe(canvas);\n\n      const onVisibility = () => {\n        if (document.hidden) pause();\n        else if (visible) resume();\n      };\n      document.addEventListener(\"visibilitychange\", onVisibility);\n\n      resume();\n\n      return () => {\n        pause();\n        io.disconnect();\n        document.removeEventListener(\"visibilitychange\", onVisibility);\n        ro.disconnect();\n        Matter.Runner.stop(runner);\n        Matter.World.remove(engine.world, mouseConstraint);\n        Matter.World.remove(engine.world, wallsRef.current);\n        wallsRef.current = [];\n      };\n    }, [engine, autoStart, addTopWall, restitution, reduced]);\n\n    const register = React.useCallback<GravityContextValue[\"register\"]>(\n      (id, element, config) => {\n        const canvas = canvasRef.current;\n        if (!canvas) return;\n        const rect = canvas.getBoundingClientRect();\n        const w = element.offsetWidth;\n        const h = element.offsetHeight;\n        const x = resolve(config.x, rect.width);\n        const y = resolve(config.y, rect.height);\n        const options: Matter.IChamferableBodyDefinition = {\n          restitution,\n          friction: 0.4,\n          angle: (config.angle * Math.PI) / 180,\n          ...config.options,\n        };\n        const body =\n          config.bodyType === \"circle\"\n            ? Matter.Bodies.circle(x, y, Math.max(w, h) / 2, options)\n            : Matter.Bodies.rectangle(x, y, w, h, {\n                ...options,\n                chamfer: { radius: Math.min(w, h) * 0.1 },\n              });\n        Matter.World.add(engine.world, body);\n        bodiesRef.current.set(id, {\n          element,\n          body,\n          isDraggable: config.isDraggable,\n        });\n      },\n      [engine, restitution],\n    );\n\n    const unregister = React.useCallback<GravityContextValue[\"unregister\"]>(\n      (id) => {\n        const entry = bodiesRef.current.get(id);\n        if (entry) Matter.World.remove(engine.world, entry.body);\n        bodiesRef.current.delete(id);\n      },\n      [engine],\n    );\n\n    const value = React.useMemo<GravityContextValue>(\n      () => ({ register, unregister, reduced }),\n      [register, unregister, reduced],\n    );\n\n    return (\n      <GravityContext.Provider value={value}>\n        <div\n          ref={canvasRef}\n          data-slot=\"gravity\"\n          className={`relative overflow-hidden [touch-action:none] ${className ?? \"\"}`}\n          {...props}\n        >\n          {children}\n        </div>\n      </GravityContext.Provider>\n    );\n  },\n);\nGravity.displayName = \"Gravity\";\n\nexport type MatterBodyProps = React.HTMLAttributes<HTMLDivElement> & {\n  /** Initial x, as `%` of the container, a px number, or px string. */\n  x?: number | string;\n  /** Initial y, as `%` of the container, a px number, or px string. */\n  y?: number | string;\n  /** Initial rotation in degrees. */\n  angle?: number;\n  /** Collision shape. Circles use the element's larger dimension as diameter. */\n  bodyType?: \"rectangle\" | \"circle\";\n  /** Whether the pointer can grab and fling this body. */\n  isDraggable?: boolean;\n  /** Escape hatch for raw Matter body options (mass, restitution, …). */\n  matterBodyOptions?: Matter.IChamferableBodyDefinition;\n};\n\nconst MatterBody = React.forwardRef<HTMLDivElement, MatterBodyProps>(\n  (\n    {\n      x = \"50%\",\n      y = \"0%\",\n      angle = 0,\n      bodyType = \"rectangle\",\n      isDraggable = true,\n      matterBodyOptions,\n      className,\n      style,\n      children,\n      ...props\n    },\n    forwardedRef,\n  ) => {\n    const ctx = React.useContext(GravityContext);\n    const id = React.useId();\n    const localRef = React.useRef<HTMLDivElement>(null);\n    React.useImperativeHandle(\n      forwardedRef,\n      () => localRef.current as HTMLDivElement,\n    );\n\n    React.useEffect(() => {\n      const element = localRef.current;\n      if (!ctx || !element || ctx.reduced) return;\n      ctx.register(id, element, {\n        x,\n        y,\n        angle,\n        bodyType,\n        isDraggable,\n        options: matterBodyOptions,\n      });\n      return () => ctx.unregister(id);\n      // Config is captured on mount; changing it remounts the body.\n    }, [ctx, id, x, y, angle, bodyType, isDraggable, matterBodyOptions]);\n\n    // Reduced motion / no context: lay the body out statically at its origin.\n    const staticPlacement =\n      !ctx || ctx.reduced\n        ? {\n            left: typeof x === \"number\" ? `${x}px` : x,\n            top: typeof y === \"number\" ? `${y}px` : y,\n            transform: `translate(-50%, -50%) rotate(${angle}deg)`,\n          }\n        : undefined;\n\n    return (\n      <div\n        ref={localRef}\n        data-slot=\"matter-body\"\n        className={`absolute top-0 left-0 will-change-transform select-none ${\n          isDraggable ? \"cursor-grab active:cursor-grabbing\" : \"\"\n        } ${className ?? \"\"}`}\n        style={{ ...staticPlacement, ...style }}\n        {...props}\n      >\n        {children}\n      </div>\n    );\n  },\n);\nMatterBody.displayName = \"MatterBody\";\n\n// Local reduced-motion hook so the file stays copy-paste self-contained without\n// pulling framer-motion just for a media query.\nfunction useReducedMotionSafe() {\n  const [reduced, setReduced] = React.useState(false);\n  React.useEffect(() => {\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    setReduced(mq.matches);\n    const onChange = () => setReduced(mq.matches);\n    mq.addEventListener(\"change\", onChange);\n    return () => mq.removeEventListener(\"change\", onChange);\n  }, []);\n  return reduced;\n}\n\nexport { Gravity, MatterBody };\n",
      "type": "registry:ui",
      "target": "components/godui/gravity.tsx"
    }
  ],
  "type": "registry:ui"
}
