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

> **This move ships as a SPEC, not a demo.** Like `matter-drop`, physics-flavoured playfulness is Phase-3-last: it transfers poorly and rarely earns its weight. **Build the `demo.html` only when a playful merchant calls for it.** Unlike `matter-drop` this needs **no physics library** — it's a lightweight vanilla move (pointer velocity + inertial decay + edge friction), so it's cheap to build when wanted. Follow the SOP (measure, degrade, verify), then remove this banner.

**Stable ID:** `draggable-throw` · **Address:** (Tier 3, **Physics** · vanilla pointer inertia — no library) · **Grafts onto:** a draggable gallery / a "toss it in the cart" prop / a scattered sticker board. · **Falls back to:** a **normal, non-draggable layout** (a static gallery or a native scroll strip).

Grabbable elements with **inertia and throw** — drag a card/prop and release, and it **coasts** with momentum, decays with friction, and **bounces gently off the edges** (or snaps back). The tactile "pick it up and fling it" feel — a draggable dish gallery, a photo you can toss, a topping you flick into a cup.

## When to use
- **A playful merchant** that wants a tactile, exploratory moment — a draggable board of dish photos, a "build your bowl" toss, a scattered sticker wall the user can shove around.
- **Secondary / exploratory content only** — never the primary menu or ordering path, and never trap the page scroll (the drag must not hijack vertical scroll; use `touch-action` carefully).
- Prefer this over `matter-drop` when you want **one grabbable object** with throw feel but **don't** need multi-body stacking/collision — it's far cheaper (no engine).

## Measured values (source: **GSAP Draggable / Observer showcase** + **award draggable-gallery sites** — capture one before building for the throw feel)
| Token / param | Value (starting point — **re-measure before building**) | Why |
|---|---|---|
| throw velocity | carry the **last ~80–100ms** of pointer velocity at release | the flick must feel like the hand's motion |
| friction (decay) | **0.92–0.95 / frame** | coasts ~0.5–0.9s then stops (self-terminating) |
| edge behaviour | **bounce ×-0.5** or **snap-back** `cubic-bezier(0.16,1,0.3,1)` | contained — the object never escapes the stage |
| max throw speed | **clamp** to a sane px/frame | a violent flick shouldn't teleport it |
| resistance | optional **rubber-band** past bounds (offset × 0.3) | premium "pull and release" feel |

## Rendering (inlined, no external files)
- **No library** — vanilla `pointerdown`/`pointermove`/`pointerup` (+ `touch` equivalents) writing `transform: translate()` (transform-only, no layout). Track velocity from the last few move deltas; on release run a **self-terminating inertia rAF** that applies friction until velocity ≈ 0.
- Can also be built on **GSAP Draggable + `inertia`** if the page is already a Tier-2 GSAP page — but for a single draggable, the vanilla version is smaller and avoids the payload (`tiers.md`: reach for the engine only for the robustness, not a single fling).

## Capability gate & fallback
Decide pre-paint (`.dthrow-on`):
- **Interactive when** JS is on and the pointer is usable. Unlike the GPU-heavy Tier-3 moves this is cheap and **touch-friendly** — a drag-to-throw is a *good* touch interaction — so it MAY run on mobile **if** it doesn't fight the page scroll (`touch-action: none` only on the draggable element, never the page). If in doubt, gate mobile → static.
- **`prefers-reduced-motion` → static layout**, no inertia (a release just stops; no coast).
- **Any fail → a normal non-draggable layout** — the gallery/props still render and are readable; they just don't move.

## ★ Heat guard
- The inertia loop is **self-terminating** (decays to a stop; nothing loops while the object is at rest). While dragging, updates are event-driven (`pointermove`), no polling.
- **Off-screen (IO) → cancel any coasting loop. Tab-hidden → cancel.** A thrown object mid-coast when you switch tabs must not keep a loop alive.

## Mobile behaviour (≤ 390px)
- Either a **touch drag-to-throw** (event-driven, heat-safe — no resident loop) **or** the static layout, depending on whether the drag can coexist with page scroll. **Default to static** unless the draggable owns a bounded stage where `touch-action:none` won't trap the scroll. Never let the drag swallow the page scroll with no exit (`qa.md`).

## Degradation (must be baked into the demo when built)
- **`prefers-reduced-motion: reduce`** → draggable but **no inertia/coast** (release = stop), or fully static; forced independent of JS for the static path.
- **No-JS** → normal static layout; all content present and crawlable.
- **A11y:** draggable props are an *enhancement* — anything meaningful (a dish name, a link) is also reachable by keyboard/normal layout; don't lock content behind a drag. Keep focus order intact.

## Performance budget (when built)
- **Payload:** ~1–2KB vanilla (0 if not GSAP); or the GSAP tax if on a Tier-2 page already.
- **Idle rAF:** 0 at rest — the coast loop self-terminates and cancels off-screen + tab-hidden. `transform`-only (compositor).
- **CLS:** 0 — dragging translates the element; it never reflows the page.

## Verify (when built)
`node scripts/verify-pattern.mjs patterns/draggable-throw/demo.html` — assert CLS < 0.03, **0 idle rAF** (off-screen + tab-hidden + on-screen-after-coast-settle), no long task, reduced-motion shows the static/non-inertial layout. Plus a supplementary probe: a synthetic drag-and-release **coasts then stops** (frames > 0 after release, 0 after settle), the object **stays within the stage** (edge bounce/snap), and the page scroll is **never trapped**.
