# Playable Objects — Physics Pill Footer

## Goal
Build a two-screen page whose second screen is a **dark footer where 12 white "pill" tags rain down from above the viewport and pile up under real 2D physics (Matter.js)**. A GSAP ScrollTrigger with `once: true` boots the physics engine the first time the footer scrolls into view; every pill is a Matter.js rigid body synced to a DOM element via a `requestAnimationFrame` loop, and a Matter `MouseConstraint` lets the user **grab, drag and fling the pills** in real time. Smooth scrolling via Lenis. The centered footer headline reads "Because why list when you can play?".

## Tech
Vanilla HTML/CSS/JS with ES module imports. Use `gsap` (npm) plus the GSAP plugin `ScrollTrigger`, `lenis` for smooth scroll, and `matter-js` for the physics simulation (`import Matter from "matter-js"`). Register the plugin with `gsap.registerPlugin(ScrollTrigger)`. No canvas rendering — Matter runs headless and you drive the DOM elements yourself.

## Layout / HTML
Two stacked full-viewport sections:

```
<section class="hero">
  <h1>Scroll down to break the laws of web design</h1>
</section>

<section class="footer">
  <div class="object-container">
    <div class="object"><p>Motionprompts</p></div>
    <div class="object"><p>HTML</p></div>
    <div class="object"><p>CSS</p></div>
    <div class="object"><p>JavaScript</p></div>
    <div class="object"><p>GSAP</p></div>
    <div class="object"><p>ScrollTrigger</p></div>
    <div class="object"><p>Lenis</p></div>
    <div class="object"><p>React</p></div>
    <div class="object"><p>Next.js</p></div>
    <div class="object"><p>WebGL</p></div>
    <div class="object"><p>Three.js</p></div>
    <div class="object"><p>Creative Dev</p></div>
  </div>

  <div class="footer-content">
    <h1>Because why list when you can play?</h1>
  </div>
</section>

<script type="module" src="./script.js"></script>
```

Key classes the JS depends on: `.object-container` (the physics world bounds) and `.object` (one per rigid body). There are **12 pills** with those exact tech-tag labels.

## Styling

**Font** — Google Fonts `DM Sans` (import the full variable range, weights 100..1000). `body { font-family: "DM Sans", sans-serif; }`.

**Reset / global**
- `* { margin: 0; padding: 0; box-sizing: border-box; }`
- `h1 { font-size: 4rem; font-weight: 500; letter-spacing: -0.04rem; line-height: 1.2; user-select: none; }`
- `section { position: relative; width: 100vw; height: 100svh; padding: 2rem; overflow: hidden; }`
- `.hero h1, .footer h1 { width: 45%; text-align: center; }`

**Hero** — `display: flex; justify-content: center; align-items: center; background: #fff; color: #0f0f0f;`.

**Footer** — `background-color: #0f0f0f; color: #fff;` (near-black on white pills is the whole palette).

**.footer-content** — `position: absolute; top: 0; left: 0; width: 100%; height: 100%; padding: 2rem; display: flex; justify-content: center; align-items: center; pointer-events: none;` with `.footer-content * { pointer-events: auto; }`. It overlays the physics layer but lets pointer events pass through to the pills.

**.object-container** — `position: absolute; top: 0; left: 0; width: 100%; height: 100%;`. Fills the footer; this rect defines the physics walls.

**.object** — `position: absolute; width: max-content; font-size: 2rem; font-weight: 500; background-color: #fff; color: #0f0f0f; padding: 1rem 2rem; border-radius: 50px; cursor: grab; user-select: none; pointer-events: auto; z-index: 2;` and `.object:active { cursor: grabbing; }`. The pills start with no `left/top` set in CSS — JS positions them every frame.

**Responsive** `@media (max-width: 1000px)`: `h1 { font-size: 2rem; }`, `.hero h1, .footer h1 { width: 100%; }`, `.object { font-size: 1rem; }`.

## GSAP + Matter.js effect (be exact)

Wrap everything in `DOMContentLoaded`.

### 1. Lenis smooth scroll wiring
```
const lenis = new Lenis();
lenis.on("scroll", ScrollTrigger.update);
gsap.ticker.add((time) => lenis.raf(time * 1000));
gsap.ticker.lagSmoothing(0);
```

### 2. Config object (exact values)
```
const config = {
  gravity: { x: 0, y: 1 },
  restitution: 0.5,
  friction: 0.15,
  frictionAir: 0.02,
  density: 0.002,
  wallThickness: 200,
  mouseStiffness: 0.6,
};
```
Module-scope state: `let engine, runner, mouseConstraint, bodies = [], topWall = null;` plus a `clamp(val, min, max)` helper.

### 3. ScrollTrigger boot (the GSAP part)
Loop over every `section`; for the one containing `.object-container`, create:
```
ScrollTrigger.create({
  trigger: section,
  start: "top bottom",   // fires as soon as the footer's top touches the viewport bottom
  once: true,            // physics initializes exactly once, ever
  onEnter: () => {
    const container = section.querySelector(".object-container");
    if (container && !engine) initPhysics(container);
  },
});
```
No scrub, no pin, no tween — the ScrollTrigger is purely a lazy-init trigger. (Keep an `animateOnScroll = true` flag; if false, fall back to initializing on `window` `load` instead.)

### 4. initPhysics(container)
- `engine = Matter.Engine.create()`; `engine.gravity = config.gravity`; solver quality cranked up: `engine.constraintIterations = 10; engine.positionIterations = 20; engine.velocityIterations = 16; engine.timing.timeScale = 1;`.
- Measure `containerRect = container.getBoundingClientRect()`.
- **Three static walls** (200px thick rectangles, all `isStatic: true`), centered just outside the container so their inner faces sit exactly on the container edges — note there is **no top wall initially** so bodies can fall in:
  - floor: center `(width/2, height + 100)`, size `(width + 400) × 200`
  - left wall: center `(-100, height/2)`, size `200 × (height + 400)`
  - right wall: center `(width + 100, height/2)`, size `200 × (height + 400)`
- **One rectangle body per `.object` element.** For each (with its measured `getBoundingClientRect()` size):
  - `startX = Math.random() * (containerRect.width - objRect.width) + objRect.width / 2` (random, fully inside horizontally)
  - `startY = -500 - index * 200` (staggered spawn heights far above the container → they fall in one after another)
  - `startRotation = (Math.random() - 0.5) * Math.PI` (random tilt ±90°), applied via `Matter.Body.setAngle`
  - body options: `restitution: 0.5, friction: 0.15, frictionAir: 0.02, density: 0.002`
  - push `{ body, element, width, height }` into `bodies` and add the body to the world.
- **Seal the ceiling after 3 seconds**: `setTimeout(..., 3000)` adds a fourth static wall at center `(width/2, -100)`, size `(width + 400) × 200`, so once everything has fallen in, flung pills can't escape upward.

### 5. Mouse dragging (MouseConstraint)
- `const mouse = Matter.Mouse.create(container);` then **remove Matter's wheel hijacking** so page scroll keeps working:
  `mouse.element.removeEventListener("mousewheel", mouse.mousewheel);` and same for `"DOMMouseScroll"`.
- `mouseConstraint = Matter.MouseConstraint.create(engine, { mouse, constraint: { stiffness: 0.6, render: { visible: false } } });` and disable the context menu: `mouseConstraint.mouse.element.oncontextmenu = () => false;`.
- **startdrag** event: remember the body's `inertia`, then `Matter.Body.setInertia(body, Infinity)` (no spin while held), `setVelocity({x:0,y:0})`, `setAngularVelocity(0)`.
- **enddrag** event: restore the original inertia (`|| 1`) and clear the dragging refs.
- **engine `beforeUpdate`**: while a body is dragged, clamp its position so it can't be pulled outside the container (`x` between `width/2` and `containerWidth - width/2`, same idea for `y`), and clamp its velocity components to **±20** — this is what makes "flinging" fast but bounded.
- Release the constraint (set `mouseConstraint.constraint.bodyB = null; ...pointB = null;`) on both container `mouseleave` and document `mouseup`, so a pill is never stuck to a pointer that left.
- Add the mouseConstraint to the world; start the sim with `runner = Matter.Runner.create(); Matter.Runner.run(runner, engine);`.

### 6. DOM sync loop
A self-scheduling `requestAnimationFrame` loop maps each physics body onto its element:
```
const x = clamp(body.position.x - width / 2, 0, containerRect.width - width);
const y = clamp(body.position.y - height / 2, -height * 3, containerRect.height - height);
element.style.left = x + "px";
element.style.top  = y + "px";
element.style.transform = `rotate(${body.angle}rad)`;
```
Positions are top-left based (`left/top`), rotation via `transform: rotate(...rad)`. The `y` clamp floor of `-height * 3` lets pills be visible slightly above the container while falling in, but the render never places them outside the box horizontally.

## Assets / images
None. The component is pure typography and CSS — no raster images, no SVG, no 3D models.

## Behavior notes
- The pills only ever spawn once (`once: true` + `!engine` guard); scrolling away and back does not re-drop them.
- Because the spawn heights are staggered (`-500 - index * 200`) the 12 pills enter the frame sequentially over ~2–3 seconds, bouncing (restitution 0.5) and settling into a pile.
- Wheel/touch scrolling stays functional over the footer because Matter's mouse wheel listeners are explicitly removed; only click-drag is captured.
- Physics + rAF DOM writes are heavy: desktop-oriented, no reduced-motion guard in the original.
- Container bounds are measured once at init; the sim is not rebuilt on resize.
