{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"gravity","type":"registry:ui","title":"Gravity","description":"Drops any elements into a real physics box where they tumble, stack, and can be grabbed and thrown.","dependencies":["matter-js"],"registryDependencies":["utils"],"files":[{"path":"registry/ui/gravity.tsx","type":"registry:ui","content":"\"use client\";\n\nimport Matter from \"matter-js\";\nimport { Children, useEffect, useRef, type ReactNode } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst { Engine, Bodies, Body, Composite, Constraint } = Matter;\n\nexport interface GravityProps {\n  /** Each direct child becomes a physics body. */\n  children: ReactNode;\n  /** Downward gravity; negative floats things up. */\n  gravity?: number;\n  /** Restitution, 0 = dead stop, 1 = superball. */\n  bounce?: number;\n  /** Surface friction between bodies. */\n  friction?: number;\n  /** Wait until the container scrolls into view before dropping. */\n  startOnView?: boolean;\n  className?: string;\n}\n\nconst STEP = 1000 / 60;\n\nexport function Gravity({\n  children,\n  gravity = 1,\n  bounce = 0.35,\n  friction = 0.2,\n  startOnView = true,\n  className,\n}: GravityProps) {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const count = Children.count(children);\n\n  useEffect(() => {\n    const root = rootRef.current!;\n    const els = Array.from(root.querySelectorAll<HTMLElement>(\":scope > [data-gravity-item]\"));\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    const engine = Engine.create({ gravity: { x: 0, y: gravity } });\n    let w = root.offsetWidth;\n    let h = root.offsetHeight;\n\n    const wall = { isStatic: true, friction: 0.6 };\n    const floor = Bodies.rectangle(w / 2, h + 50, 6000, 100, wall);\n    const left = Bodies.rectangle(-50, 0, 100, 6000, wall);\n    const right = Bodies.rectangle(w + 50, 0, 100, 6000, wall);\n\n    const items = els.map((el, i) => {\n      const bw = el.offsetWidth;\n      const bh = el.offsetHeight;\n      const x = bw / 2 + Math.random() * Math.max(1, w - bw);\n      const body = Bodies.rectangle(x, -bh - i * 40, bw, bh, {\n        chamfer: { radius: Math.min(bw, bh) / 2 - 0.5 },\n        angle: (Math.random() - 0.5) * 0.8,\n        restitution: bounce,\n        friction,\n        frictionAir: 0.012,\n      });\n      return { el, body, bw, bh };\n    });\n    Composite.add(engine.world, [floor, left, right, ...items.map((it) => it.body)]);\n\n    const sync = () => {\n      for (const { el, body, bw, bh } of items) {\n        // Anything flung out of bounds gets dropped back in from the top.\n        if (body.position.y > h + 400 || body.position.x < -400 || body.position.x > w + 400) {\n          Body.setPosition(body, { x: w / 2, y: -bh });\n          Body.setVelocity(body, { x: 0, y: 0 });\n        }\n        el.style.transform = `translate3d(${body.position.x - bw / 2}px, ${body.position.y - bh / 2}px, 0) rotate(${body.angle}rad)`;\n        el.style.visibility = \"visible\";\n      }\n    };\n\n    let raf = 0;\n    let last = 0;\n    let acc = 0;\n    let visible = true;\n    const loop = (now: number) => {\n      acc += Math.min(now - (last || now), 50);\n      last = now;\n      // Fixed timestep keeps the simulation identical on 60 Hz and 120 Hz screens.\n      while (acc >= STEP) {\n        Engine.update(engine, STEP);\n        acc -= STEP;\n      }\n      sync();\n      raf = visible ? requestAnimationFrame(loop) : 0;\n      if (!raf) last = 0;\n    };\n    const start = () => {\n      if (!raf && visible) raf = requestAnimationFrame(loop);\n    };\n    if (reduced) {\n      // Skip the fall: settle the pile off-screen, then show it at rest.\n      for (let i = 0; i < 600; i++) Engine.update(engine, STEP);\n      sync();\n    }\n\n    const io = new IntersectionObserver(\n      ([entry]) => {\n        visible = entry.isIntersecting;\n        if (visible) start();\n      },\n      { threshold: 0.2 },\n    );\n    io.observe(root);\n    if (!startOnView) start();\n\n    const ro = new ResizeObserver(() => {\n      w = root.offsetWidth;\n      h = root.offsetHeight;\n      Body.setPosition(floor, { x: w / 2, y: h + 50 });\n      Body.setPosition(right, { x: w + 50, y: 0 });\n    });\n    ro.observe(root);\n\n    // Drag and throw: pin the grabbed body to the pointer with a soft constraint.\n    let drag: { c: Matter.Constraint; el: HTMLElement } | null = null;\n    const local = (e: PointerEvent) => {\n      const r = root.getBoundingClientRect();\n      return { x: e.clientX - r.left, y: e.clientY - r.top };\n    };\n    const onDown = (e: PointerEvent) => {\n      const it = items.find((i) => i.el.contains(e.target as Node));\n      if (!it) return;\n      const p = local(e);\n      const c = Constraint.create({\n        pointA: p,\n        bodyB: it.body,\n        pointB: { x: p.x - it.body.position.x, y: p.y - it.body.position.y },\n        stiffness: 0.2,\n        damping: 0.1,\n        length: 0,\n      });\n      Composite.add(engine.world, c);\n      it.el.setPointerCapture(e.pointerId);\n      it.el.style.cursor = \"grabbing\";\n      drag = { c, el: it.el };\n      start();\n    };\n    const onMove = (e: PointerEvent) => {\n      if (drag) drag.c.pointA = local(e);\n    };\n    const onUp = () => {\n      if (!drag) return;\n      Composite.remove(engine.world, drag.c);\n      drag.el.style.cursor = \"grab\";\n      drag = null;\n    };\n    root.addEventListener(\"pointerdown\", onDown);\n    root.addEventListener(\"pointermove\", onMove);\n    root.addEventListener(\"pointerup\", onUp);\n    root.addEventListener(\"pointercancel\", onUp);\n\n    return () => {\n      cancelAnimationFrame(raf);\n      io.disconnect();\n      ro.disconnect();\n      root.removeEventListener(\"pointerdown\", onDown);\n      root.removeEventListener(\"pointermove\", onMove);\n      root.removeEventListener(\"pointerup\", onUp);\n      root.removeEventListener(\"pointercancel\", onUp);\n      Composite.clear(engine.world, false);\n      Engine.clear(engine);\n    };\n  }, [gravity, bounce, friction, startOnView, count]);\n\n  return (\n    <div ref={rootRef} className={cn(\"relative overflow-hidden\", className)}>\n      {Children.map(children, (child) => (\n        <div\n          data-gravity-item=\"\"\n          style={{\n            position: \"absolute\",\n            left: 0,\n            top: 0,\n            visibility: \"hidden\",\n            touchAction: \"none\",\n            userSelect: \"none\",\n            cursor: \"grab\",\n            willChange: \"transform\",\n          }}\n        >\n          {child}\n        </div>\n      ))}\n    </div>\n  );\n}\n"}]}