# `matter-drop` — spec  ·  ⚠️ OPTIONAL / SPEC-ONLY (build on demand)

> **This move ships as a SPEC, not a demo.** Physics is Phase-3-last on purpose: it **transfers poorly** (needs bespoke layout, rarely survives a mobile recompose) and rarely earns its weight — the reverse of why motion is worth library-ifying (`taxonomy.md`, `measured-not-vibes.md`). **Build the `demo.html` only when a genuinely playful merchant calls for it** (a candy/sticker/toppings brand); until then this spec is the contract. Follow the SOP: measure values from a real award physics site, bake the two degradations, run `verify-pattern.mjs`, then remove this banner.

**Stable ID:** `matter-drop` · **Address:** (Tier 3, **Physics** · inlined Matter.js) · **Grafts onto:** a `[data-mod="specials"]` / brand-playful section or a hero prop shelf. · **Falls back to:** a **static, pre-arranged pile** (the same stickers/ingredients laid out in CSS, no simulation).

Branded objects — **stickers, ingredients, toppings, coupons** — **drop and stack** under gravity, collide, settle into a pile, and can be **flung** with the pointer. The playful, tactile "toy box" moment for a brand whose personality is fun (bubble tea toppings raining into a cup, taco-truck stickers piling up).

## When to use
- **Only a playful brand** whose voice is genuinely fun — candy, boba toppings, kids' menu, a sticker-culture taco truck. On a refined/minimal brand it reads as a gimmick (off-pole — `measured-not-vibes.md`).
- **One moment per site.** A physics pile is a spectacle; a second one is noise.
- **Never** for core content (menu, NAP, ordering) — decoration only, because it's the least reliable move to lay out responsively.

## Measured values (source: **award Matter.js sites** — capture a real one before building: e.g. a playful product site's drop-in hero + `brm.io/matter-js` demos — for restitution / density / drop cadence)
| Token / param | Value (starting point — **re-measure before building**) | Why |
|---|---|---|
| gravity `y` | **1.0** (Matter default) | natural fall; lower = floaty/dreamy |
| restitution | **0.35–0.5** | a little bounce, not rubber |
| friction | **0.3**, frictionAir **0.02** | objects settle instead of sliding forever |
| drop cadence | **1 object / 180–260ms**, ~**14–22 total** | a satisfying rain that ends (not endless) |
| body size | **40–72px** | thumb-scale props; texture-mapped sprites |
| settle → sleep | enable `Sleeping`; **stop the loop once all bodies sleep** | the heat guard (see below) |

## Rendering (inlined, no external files — HARD OUTPUT RULE)
- **Matter.js source is INLINED** in a `<script>` block (like the Tier-2 GSAP moves inline GSAP) — **never a CDN, never a local file.** Matter's minified core is ~**85–90KB**; register/use only what you need (`Engine`, `World`, `Bodies`, `Composite`, `Mouse`, `Runner` — or hand-roll `Engine.update` yourself and skip `Runner`).
- Bodies are **texture-mapped** with the brand's sprite images (the props are the asset — a playful brand usually has stickers/illustrations ready). Render to a `<canvas>`.
- Because of the payload, this is **showcase / flagship-tier only** — same tax rationale as Tier-2 (`tiers.md`).

## ★ Capability gate & fallback (physics is a heavy, heat-prone Tier-3 — gate hard)
Decide in a **pre-paint `<head>` script** (`.mdrop-on`), no post-paint swap:
- **Runs only when** `!mobile(≤390 / coarse) && !prefers-reduced-motion` (a WebGL check isn't needed — Matter renders to canvas-2d, but the cost gate still applies).
- **Any fail → the static pre-arranged pile** — the same props laid out in CSS at their "settled" positions. Nothing is hidden; it just doesn't simulate.
- **NEVER run the physics engine on a phone** — a continuous solver loop is a top battery/heat offender. Mobile always gets the static pile.

## ★ The heat guard (the make-or-break of a physics move)
- The solver loop **MUST self-terminate.** Enable Matter's **`Sleeping`** and **stop `Engine.update`/`Runner` once every body is asleep** (the pile has settled) — do **not** run the solver forever. A poke/fling wakes it and restarts the loop; it re-sleeps and stops again.
- **Off-screen (IntersectionObserver) → stop the runner. Tab-hidden (`visibilitychange`) → stop.** Same two gates as every move.
- If you keep `Matter.Runner`, note it uses its own rAF — **gate it** the way the GSAP ticker had to be gated (`qa.md` Tier-2 note): a hard rAF gate, or call `Runner.stop()` / `Engine.clear()` on the idle/off-screen/hidden transitions. Verify with the idle-rAF probe's two legs **plus an on-screen-after-settle** check (the pile must stop the loop once asleep, even while on-screen).

## Mobile behaviour (≤ 390px / coarse pointer)
- **Static pre-arranged pile.** Engine never initialised; no Matter parse beyond the (still-inlined, but inert) source. No solver, no rAF, no fling handlers.

## Degradation (must be baked into the demo when built)
- **`prefers-reduced-motion: reduce`** → the static settled pile; forced in CSS.
- **No-JS** → the static pile (CSS-positioned sprites); all props visible, nothing depends on the sim.
- **A11y:** the pile is decorative (`aria-hidden`); any real content (a specials headline, prices) lives in normal text beside it, never as physics bodies.

## Performance budget (when built)
- **Payload:** ~85–90KB inlined Matter core + sprite bytes — showcase/flagship only.
- **Idle rAF:** 0 once the pile sleeps / off-screen / tab-hidden (the whole point of the heat guard).
- **Cap:** **≤ ~24 bodies.** A physics solver is O(bodies²)-ish for collisions; keep the count low for a stable 60fps on mid GPUs.
- **CLS:** 0 — the canvas box is fixed; the sim moves sprites *inside* it, never the page.

## Verify (when built)
`node scripts/verify-pattern.mjs patterns/matter-drop/demo.html` — assert CLS < 0.05, **0 idle rAF once settled** (off-screen + tab-hidden + **on-screen-after-sleep**), long task < 120ms (one-time Matter parse off the mobile path), reduced-motion shows the static pile. Plus a supplementary probe: the solver **wakes on a fling and re-sleeps** (loop restarts then stops), and the **capability gate falls back to the static pile** on mobile / reduced-motion.
