// zones.jsx — the three playground attractions, living inside the open world.
// Each takes a `wild` multiplier and an optional `tick` clock. They handle
// their own drag/click logic; pointerdown on interactive elements stopPropagation
// so the world doesn't try to pan from on top of them.

const { useState: zUseState, useEffect: zUseEffect, useRef: zUseRef, useMemo: zUseMemo } = React;

// ─────────────────────────────────────────────────────────────────────────
//   SLIDE ZONE — kinetic type
// ─────────────────────────────────────────────────────────────────────────
function SlideZone({ wild = 1 }) {
  const stageRef = zUseRef(null);
  const railRef  = zUseRef(null);
  const lettersRef = zUseRef([]);
  const pileRef    = zUseRef([]);
  const idRef      = zUseRef(1);
  const [, force]  = zUseState(0);
  const dragRef    = zUseRef(null);

  // physics loop
  zUseEffect(() => {
    let raf;
    const step = () => {
      const stage = stageRef.current;
      if (!stage) { raf = requestAnimationFrame(step); return; }
      const W = stage.clientWidth, H = stage.clientHeight;
      const G = 0.5 * wild;

      // slope endpoints (matches the SVG rail)
      const slope = { x1: W*0.92, y1: H*0.18, x2: H*0.78 ? W*0.18 : W*0.18, y2: H*0.78 };
      const slopeY = (xv) => {
        const t = clamp((xv - slope.x1) / (slope.x2 - slope.x1), 0, 1);
        return slope.y1 + (slope.y2 - slope.y1) * t;
      };

      const next = [];
      for (const L of lettersRef.current) {
        let { x, y, vx, vy, rot, vrot, char, color, scale, life } = L;
        vy += G;
        x += vx; y += vy;
        rot += vrot;

        if (x > W*0.18 && x < W*0.92) {
          const sy = slopeY(x);
          if (y > sy && vy > 0) {
            y = sy;
            const dx = (slope.x2 - slope.x1), dy = (slope.y2 - slope.y1);
            const len = Math.hypot(dx, dy);
            const nx = -dy/len, ny = dx/len;
            const dot = vx*nx + vy*ny;
            vx -= 1.6 * dot * nx; vy -= 1.6 * dot * ny;
            vx *= 0.86; vy *= 0.86;
            vrot *= 0.92;
          }
        }
        // floor — settle to pile
        if (y > H - 30) {
          pileRef.current.push({
            id: idRef.current++,
            x, y: H - 14 - Math.random()*4,
            char, color, rot: rot + (Math.random()-.5)*30, scale: scale*0.9,
          });
          if (pileRef.current.length > 220) pileRef.current.splice(0, 40);
          continue;
        }
        // walls
        if (x < 8) { x = 8; vx = -vx*0.6; }
        if (x > W-8) { x = W-8; vx = -vx*0.6; }
        life -= 1;
        next.push({ ...L, x, y, vx, vy, rot, vrot, life });
      }
      lettersRef.current = next;
      force((n) => (n + 1) % 1e6);
      raf = requestAnimationFrame(step);
    };
    raf = requestAnimationFrame(step);
    return () => cancelAnimationFrame(raf);
  }, [wild]);

  const launch = (char, fromX, fromY, opts = {}) => {
    const stage = stageRef.current;
    if (!stage) return;
    const r = stage.getBoundingClientRect();
    const x = (fromX != null ? fromX - r.left : rand(r.width*0.7, r.width*0.92));
    const y = (fromY != null ? fromY - r.top  : rand(40, 80));
    const colors = ["var(--cherry)","var(--sky)","var(--grass)","var(--gum)","var(--ink)","var(--sun)","var(--plum)"];
    lettersRef.current.push({
      id: idRef.current++,
      x, y,
      vx: opts.vx ?? rand(-2, 0.5),
      vy: opts.vy ?? rand(0.2, 1.6),
      rot: rand(-20, 20),
      vrot: rand(-6, 6),
      char,
      color: opts.color ?? choose(colors),
      scale: opts.scale ?? rand(0.9, 1.4),
      life: 1200,
    });
  };

  // listen to global keypress when slide stage is in viewport center-ish (always-on inside world)
  zUseEffect(() => {
    const onKey = (e) => {
      if (e.target && e.target.matches && e.target.matches("input,textarea,select")) return;
      if (e.metaKey || e.ctrlKey || e.altKey) return;
      const k = e.key;
      // skip pan keys (wasd) so panning the world doesn't spew letters
      if (k.length === 1 && /[A-Za-z0-9!?@#&*+=]/.test(k) && !"wasdWASD".includes(k)) {
        launch(k.toUpperCase(), null, null);
      } else if (k === " ") {
        e.preventDefault();
        ["F","R","G","M","T"].forEach((c, i) =>
          setTimeout(() => launch(c, null, null, { scale: 1.6 }), i * 70));
      } else if (k === "Backspace") {
        lettersRef.current = []; pileRef.current = [];
        force((n) => (n + 1) % 1e6);
      }
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, []);

  // click in stage to launch from cursor
  const onStageDown = (e) => {
    if (e.target.closest("[data-rack]")) return;
    e.stopPropagation();
    launch(choose("FRGMT".split("")), e.clientX, e.clientY,
           { vx: rand(-1,1), vy: rand(-2,2), scale: rand(1.2,1.8) });
  };

  // drag from rack
  const startDrag = (char, e) => {
    e.preventDefault(); e.stopPropagation();
    dragRef.current = { char, x: e.clientX, y: e.clientY };
    force((n) => (n+1)%1e6);
    const move = (ev) => { dragRef.current = { char, x: ev.clientX, y: ev.clientY }; force((n)=>(n+1)%1e6); };
    const up = (ev) => {
      window.removeEventListener("mousemove", move);
      window.removeEventListener("mouseup", up);
      launch(char, ev.clientX, ev.clientY, { vx: rand(-1,1), vy: rand(0,1), scale: rand(1.1,1.6) });
      dragRef.current = null;
      force((n)=>(n+1)%1e6);
    };
    window.addEventListener("mousemove", move);
    window.addEventListener("mouseup", up);
  };

  return (
    <div ref={stageRef} onMouseDown={onStageDown}
      style={{
        position:"absolute",
        left:0, top:0, width:"100%", height:"100%",
        overflow:"hidden",
      }}>
      {/* title slab */}
      <div style={{ position:"absolute", left:24, top:24, zIndex:10, maxWidth:300 }}>
        <div style={{ display:"flex", gap:8, marginBottom:10 }}>
          <Tag color="var(--cherry)">01 / type</Tag>
          <Tag>kinetic</Tag>
        </div>
        <div style={{ fontFamily:"var(--display)", fontWeight:900, fontSize:72, lineHeight:.86, letterSpacing:"-.04em" }}>
          the<br/>
          <span style={{ fontStyle:"italic", fontFamily:"var(--serif)", fontWeight:400 }}>slide</span>.
        </div>
        <div style={{ marginTop:10, fontFamily:"var(--serif)", fontStyle:"italic", fontSize:18, color:"var(--ink-2)", lineHeight:1.3, maxWidth:280 }}>
          letters fall, ride the rail, bump into the floor and stay there.
        </div>
        <div style={{ marginTop:14, fontFamily:"var(--mono)", fontSize:11, letterSpacing:".06em", textTransform:"uppercase", display:"flex", flexDirection:"column", gap:5, opacity:.78 }}>
          <Hint k="type">launch a letter</Hint>
          <Hint k="space">drop FRGMT</Hint>
          <Hint k="click">throw from cursor</Hint>
          <Hint k="back">clear the pile</Hint>
        </div>
      </div>

      {/* slide rail */}
      <svg ref={railRef} style={{ position:"absolute", inset:0, width:"100%", height:"100%", pointerEvents:"none" }}>
        <line x1="92%" y1="18%" x2="18%" y2="78%" stroke="var(--ink)" strokeWidth="14" strokeLinecap="round" />
        <line x1="92%" y1="18%" x2="18%" y2="78%" stroke="var(--cherry)" strokeWidth="6" strokeLinecap="round" />
        <line x1="92%" y1="18%" x2="92%" y2="14%" stroke="var(--ink)" strokeWidth="3" />
        <circle cx="92%" cy="14%" r="6" fill="var(--ink)" />
        <line x1="0" y1="calc(100% - 16px)" x2="100%" y2="calc(100% - 16px)" stroke="var(--ink)" strokeWidth="1.5" strokeDasharray="2 4" />
      </svg>

      {/* falling letters */}
      {lettersRef.current.map(L => (
        <span key={L.id} style={{
          position:"absolute", left:L.x, top:L.y,
          transform:`translate(-50%, -50%) rotate(${L.rot}deg) scale(${L.scale})`,
          fontFamily:"var(--display)", fontWeight:900, fontSize:64,
          color:L.color, lineHeight:1, pointerEvents:"none",
          willChange:"transform, top, left",
        }}>{L.char}</span>
      ))}

      {/* pile */}
      {pileRef.current.map(p => (
        <span key={p.id} style={{
          position:"absolute", left:p.x, top:p.y,
          transform:`translate(-50%, -50%) rotate(${p.rot}deg) scale(${p.scale})`,
          fontFamily:"var(--display)", fontWeight:900, fontSize:48,
          color:p.color, lineHeight:1, pointerEvents:"none",
        }}>{p.char}</span>
      ))}

      {/* letter rack */}
      <div data-rack style={{
        position:"absolute", right:18, top:24, zIndex:30,
        display:"flex", flexDirection:"column", gap:8, alignItems:"flex-end",
      }}
        onMouseDown={(e) => e.stopPropagation()}
      >
        <div style={{ fontFamily:"var(--mono)", fontSize:10, letterSpacing:".1em", textTransform:"uppercase", opacity:.6 }}>
          rack — drag a letter
        </div>
        <div style={{ display:"grid", gridTemplateColumns:"repeat(5, 32px)", gap:5 }}>
          {[..."ABCDEFGHIJKLMNOPQRSTUVWXYZ?!&"].map((c) => (
            <button key={c} data-cursor="grab"
              onMouseDown={(e) => startDrag(c, e)}
              style={{
                width:32, height:32,
                display:"flex", alignItems:"center", justifyContent:"center",
                fontFamily:"var(--display)", fontWeight:800, fontSize:16,
                background:"var(--paper)", border:"1px solid var(--ink)",
                borderRadius:6, boxShadow:"2px 2px 0 var(--ink)",
              }}>
              {c}
            </button>
          ))}
        </div>
      </div>

      {/* drag ghost */}
      {dragRef.current && (
        <span style={{
          position:"fixed", left:dragRef.current.x, top:dragRef.current.y, zIndex:9300,
          transform:"translate(-50%, -50%) rotate(-6deg)",
          fontFamily:"var(--display)", fontWeight:900, fontSize:64,
          color:"var(--cherry)", pointerEvents:"none",
        }}>{dragRef.current.char}</span>
      )}
    </div>
  );
}

// ─────────────────────────────────────────────────────────────────────────
//   SANDBOX ZONE — shape physics
// ─────────────────────────────────────────────────────────────────────────
const SHAPES = ["square","circle","triangle","diamond","halfcircle","cross","arch","star"];
const SAND_COLORS = ["var(--cherry)","var(--sun)","var(--sky)","var(--grass)","var(--gum)","var(--ink)","var(--paper)","var(--plum)"];

function SandboxZone({ wild = 1 }) {
  const stageRef = zUseRef(null);
  const [pieces, setPieces] = zUseState(() => seedSandPieces());
  const [drag, setDrag] = zUseState(null);
  const [colorIdx, setColorIdx] = zUseState(0);
  const idRef = zUseRef(50);

  const place = (shape, color, x, y, opts = {}) => {
    setPieces((p) => [...p, {
      id: idRef.current++,
      shape, color, x, y,
      size: opts.size ?? 80,
      rot: opts.rot ?? rand(-8, 8),
    }]);
  };

  zUseEffect(() => {
    const stage = stageRef.current;
    if (!stage) return;
    const onWheel = (e) => {
      const piece = e.target.closest("[data-piece-id]");
      if (!piece) return;
      e.preventDefault();
      const id = +piece.dataset.pieceId;
      setPieces((arr) => arr.map(p => {
        if (p.id !== id) return p;
        if (e.shiftKey) return { ...p, size: clamp(p.size + (e.deltaY < 0 ? 8 : -8), 24, 240) };
        return { ...p, rot: p.rot + (e.deltaY < 0 ? -8 : 8) };
      }));
    };
    stage.addEventListener("wheel", onWheel, { passive:false });
    return () => stage.removeEventListener("wheel", onWheel);
  }, []);

  const startPaletteDrag = (shape, e) => {
    e.preventDefault(); e.stopPropagation();
    setDrag({ from:"palette", shape, color: SAND_COLORS[colorIdx], x: e.clientX, y: e.clientY });
  };
  const startPieceDrag = (id, e) => {
    e.stopPropagation();
    const piece = pieces.find(p => p.id === id);
    if (!piece) return;
    const stage = stageRef.current.getBoundingClientRect();
    setDrag({ from:"piece", id,
      x: e.clientX, y: e.clientY,
      dx: e.clientX - (stage.left + piece.x),
      dy: e.clientY - (stage.top + piece.y),
    });
  };

  // drag effect: only attach listeners when transitioning to a drag, not every move.
  // Use a ref to read the latest drag in the closure.
  const dragLatest = zUseRef(drag);
  dragLatest.current = drag;
  const wildLatest = zUseRef(wild);
  wildLatest.current = wild;
  zUseEffect(() => {
    if (!drag) return;
    const onMove = (e) => setDrag(d => d && ({ ...d, x: e.clientX, y: e.clientY }));
    const onUp = (e) => {
      const d = dragLatest.current;
      if (!d) return;
      const stage = stageRef.current.getBoundingClientRect();
      const inX = e.clientX - stage.left, inY = e.clientY - stage.top;
      const inside = inX > 0 && inX < stage.width && inY > 0 && inY < stage.height;
      if (d.from === "palette" && inside) {
        place(d.shape, d.color, inX, inY, { size: rand(60,120), rot: rand(-20,20) * wildLatest.current });
      } else if (d.from === "piece") {
        if (inside) {
          setPieces(arr => arr.map(p => p.id === d.id ? { ...p, x: inX - (d.dx ?? 0), y: inY - (d.dy ?? 0) } : p));
        } else {
          setPieces(arr => arr.filter(p => p.id !== d.id));
        }
      }
      setDrag(null);
    };
    window.addEventListener("mousemove", onMove);
    window.addEventListener("mouseup", onUp);
    return () => {
      window.removeEventListener("mousemove", onMove);
      window.removeEventListener("mouseup", onUp);
    };
  }, [!!drag]); // only re-run when drag transitions on/off

  const onPieceClick = (id, e) => {
    e.stopPropagation();
    setPieces(arr => arr.map(p => {
      if (p.id !== id) return p;
      const ci = (SAND_COLORS.indexOf(p.color) + 1) % SAND_COLORS.length;
      return { ...p, color: SAND_COLORS[ci] };
    }));
  };
  const onPieceDouble = (id, e) => {
    e.stopPropagation();
    setPieces(arr => arr.filter(p => p.id !== id));
  };

  const shake = () => {
    const stage = stageRef.current.getBoundingClientRect();
    setPieces(arr => arr.map(p => ({
      ...p,
      x: rand(60, stage.width - 60),
      y: rand(80, stage.height - 100),
      rot: rand(-30, 30) * wild,
    })));
  };
  const tidy = () => {
    const G = 96;
    const stage = stageRef.current.getBoundingClientRect();
    const cols = Math.max(1, Math.floor((stage.width - 120) / G));
    setPieces(arr => arr.map((p, i) => ({
      ...p,
      x: 60 + (i % cols) * G + G/2,
      y: 100 + Math.floor(i / cols) * G + G/2,
      rot: 0, size: 64,
    })));
  };
  const clear = () => setPieces([]);

  const onStageClick = (e) => {
    if (e.target.closest("[data-no-place], [data-piece-id]")) return;
    e.stopPropagation();
    const stage = stageRef.current.getBoundingClientRect();
    place(choose(SHAPES), SAND_COLORS[colorIdx], e.clientX - stage.left, e.clientY - stage.top, { size: rand(60,120), rot: rand(-20,20) });
  };

  return (
    <div ref={stageRef} onMouseDown={onStageClick}
      style={{ position:"absolute", inset:0, overflow:"hidden" }}>
      {/* sand grid */}
      <div style={{
        position:"absolute", inset:0,
        backgroundImage:"radial-gradient(circle at 1px 1px, rgba(14,14,14,.18) 1px, transparent 1px)",
        backgroundSize:"22px 22px", opacity:.55, pointerEvents:"none",
      }} />

      {/* dashed frame */}
      <div style={{
        position:"absolute", left:24, right:24, top:24, bottom:104,
        border:"1.5px dashed rgba(14,14,14,.18)", borderRadius:18,
        pointerEvents:"none",
      }} />

      {/* title */}
      <div data-no-place style={{ position:"absolute", left:24, top:36, zIndex:10, maxWidth:280 }}
        onMouseDown={(e) => e.stopPropagation()}>
        <div style={{ display:"flex", gap:8, marginBottom:10 }}>
          <Tag color="var(--sun)">02 / shape</Tag>
          <Tag>tactile</Tag>
        </div>
        <div style={{ fontFamily:"var(--display)", fontWeight:900, fontSize:68, lineHeight:.86, letterSpacing:"-.04em" }}>
          the<br/>
          <span style={{ fontStyle:"italic", fontFamily:"var(--serif)", fontWeight:400 }}>sandbox</span>.
        </div>
        <div style={{ marginTop:10, fontFamily:"var(--serif)", fontStyle:"italic", fontSize:18, color:"var(--ink-2)", lineHeight:1.3, maxWidth:260 }}>
          drag a primitive in. wheel to spin. shift-wheel to grow.
        </div>
        <div style={{ marginTop:12, fontFamily:"var(--mono)", fontSize:11, letterSpacing:".06em", textTransform:"uppercase", display:"flex", flexDirection:"column", gap:5, opacity:.78 }}>
          <Hint k="drag">to place a shape</Hint>
          <Hint k="click">to recolor</Hint>
          <Hint k="wheel">to rotate · ⇧ scale</Hint>
          <Hint k="2×">to remove</Hint>
        </div>
      </div>

      {/* pieces */}
      {pieces.map(p => (
        <div key={p.id}
          data-piece-id={p.id} data-cursor="grab"
          onMouseDown={(e) => startPieceDrag(p.id, e)}
          onClick={(e) => onPieceClick(p.id, e)}
          onDoubleClick={(e) => onPieceDouble(p.id, e)}
          style={{
            position:"absolute", left:p.x, top:p.y,
            width:p.size, height:p.size,
            marginLeft:-p.size/2, marginTop:-p.size/2,
            transform:`rotate(${p.rot}deg)`,
            transition: drag && drag.from === "piece" && drag.id === p.id ? "none" : "transform .15s",
          }}>
          <ShapeSVG shape={p.shape} color={p.color} />
        </div>
      ))}

      {/* palette + actions — bottom strip */}
      <div data-no-place style={{
        position:"absolute", left:"50%", bottom:18, transform:"translateX(-50%)", zIndex:30,
        display:"flex", gap:14, alignItems:"center",
        padding:"10px 14px",
        background:"var(--paper)", border:"1.5px solid var(--ink)", borderRadius:99,
        boxShadow:"4px 4px 0 var(--ink)",
      }} onMouseDown={(e) => e.stopPropagation()}>
        {SHAPES.map((s) => (
          <button key={s} data-cursor="grab"
            onMouseDown={(e) => startPaletteDrag(s, e)}
            style={{ width:38, height:38, display:"flex", alignItems:"center", justifyContent:"center", borderRadius:8, padding:4 }}>
            <ShapeSVG shape={s} color={SAND_COLORS[colorIdx]} />
          </button>
        ))}
        <span style={{ width:1, height:30, background:"rgba(14,14,14,.2)" }} />
        <div style={{ display:"flex", gap:6 }}>
          {SAND_COLORS.map((c, i) => (
            <button key={c} data-cursor="hot"
              onClick={(e) => { e.stopPropagation(); setColorIdx(i); }}
              onMouseDown={(e) => e.stopPropagation()}
              style={{
                width:18, height:18, borderRadius:99, background:c,
                border:"1.5px solid var(--ink)",
                outline: i === colorIdx ? "2px solid var(--ink)" : "none",
                outlineOffset: 3,
              }}/>
          ))}
        </div>
        <span style={{ width:1, height:30, background:"rgba(14,14,14,.2)" }} />
        <SandActionBtn onClick={shake} hue="var(--sun)">shake</SandActionBtn>
        <SandActionBtn onClick={tidy}  hue="var(--sky)">tidy</SandActionBtn>
        <SandActionBtn onClick={clear} hue="var(--cherry)">clear</SandActionBtn>
      </div>

      {/* drag ghost */}
      {drag && drag.from === "palette" && (
        <div style={{
          position:"fixed", left:drag.x, top:drag.y, zIndex:9300,
          width:80, height:80, marginLeft:-40, marginTop:-40,
          pointerEvents:"none", transform:"rotate(-6deg)",
        }}>
          <ShapeSVG shape={drag.shape} color={drag.color} />
        </div>
      )}
    </div>
  );
}
function SandActionBtn({ children, onClick, hue }) {
  return (
    <button data-cursor="hot"
      onClick={(e) => { e.stopPropagation(); onClick(); }}
      onMouseDown={(e) => e.stopPropagation()}
      style={{
        padding:"6px 12px", background:hue, color:"var(--ink)",
        border:"1.5px solid var(--ink)", borderRadius:99,
        boxShadow:"3px 3px 0 var(--ink)",
        fontFamily:"var(--mono)", fontSize:11, letterSpacing:".08em", textTransform:"uppercase",
      }}>{children}</button>
  );
}
function seedSandPieces() {
  return [
    { id:1, shape:"square",     color:"var(--cherry)", x:200, y:280, size:84, rot:-6 },
    { id:2, shape:"circle",     color:"var(--sun)",    x:340, y:340, size:104, rot:0 },
    { id:3, shape:"triangle",   color:"var(--sky)",    x:480, y:280, size:90, rot:8 },
    { id:4, shape:"halfcircle", color:"var(--gum)",    x:620, y:340, size:96, rot:-12 },
    { id:5, shape:"diamond",    color:"var(--grass)",  x:520, y:200, size:64, rot:14 },
    { id:6, shape:"star",       color:"var(--plum)",   x:300, y:200, size:74, rot:-4 },
  ];
}

// ─────────────────────────────────────────────────────────────────────────
//   SWINGS ZONE — color pendulums
// ─────────────────────────────────────────────────────────────────────────
const SWING_COLORS = [
  { name:"cherry", css:"#E8553A" },
  { name:"sun",    css:"#F1C242" },
  { name:"grass",  css:"#5BB37A" },
  { name:"sky",    css:"#3A6FE0" },
  { name:"gum",    css:"#E78AAF" },
];

function SwingsZone({ wild = 1 }) {
  const stageRef = zUseRef(null);
  const canvasRef = zUseRef(null);
  const [size, setSize] = zUseState({ w: 0, h: 0 });
  const swingsRef = zUseRef(null);
  const [, force] = zUseState(0);
  const dragRef = zUseRef(null);
  const [released, setReleased] = zUseState(true);
  const trailDecay = 0.97;
  const damping    = 0.0015;

  // resize
  zUseEffect(() => {
    const onResize = () => {
      const s = stageRef.current; if (!s) return;
      const r = s.getBoundingClientRect();
      setSize({ w: r.width, h: r.height });
      const c = canvasRef.current;
      if (!c) return;
      const dpr = Math.min(window.devicePixelRatio || 1, 2);
      c.width = r.width * dpr;
      c.height = r.height * dpr;
      c.style.width = r.width + "px";
      c.style.height = r.height + "px";
      const ctx = c.getContext("2d");
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    };
    onResize();
    window.addEventListener("resize", onResize);
    return () => window.removeEventListener("resize", onResize);
  }, []);

  // init swings
  zUseEffect(() => {
    if (!size.w) return;
    const beamY = size.h * 0.18;
    const N = SWING_COLORS.length;
    const pad = 80;
    const span = Math.max(120, size.w - pad * 2);
    const swings = SWING_COLORS.map((c, i) => {
      const ax = pad + (span * (i + 0.5)) / N;
      return {
        id: i,
        anchor: { x: ax, y: beamY },
        length: size.h * 0.55,
        angle: ((i - N/2) * 0.18) * (1 + Math.random()*0.2),
        av: 0,
        radius: 24,
        color: c.css,
        x: 0, y: 0,
      };
    });
    swingsRef.current = swings;
    force((n) => n + 1);
  }, [size.w, size.h]);

  // anim loop
  zUseEffect(() => {
    if (!size.w) return;
    let raf;
    const c = canvasRef.current; if (!c) return;
    const ctx = c.getContext("2d");
    const G = 0.0014 * wild;
    const tick = () => {
      const arr = swingsRef.current;
      if (!arr) { raf = requestAnimationFrame(tick); return; }
      for (const s of arr) {
        if (released && !(dragRef.current && dragRef.current.id === s.id)) {
          const acc = -G * 1000 * Math.sin(s.angle) / Math.sqrt(s.length / 200);
          s.av += acc;
          s.av *= (1 - damping);
          s.angle += s.av * 0.016;
        } else if (dragRef.current && dragRef.current.id === s.id) {
          const d = dragRef.current;
          const dx = d.x - s.anchor.x, dy = d.y - s.anchor.y;
          const len = Math.hypot(dx, dy) || 1;
          const ang = Math.atan2(dx, dy);
          s.angle = ang;
          s.av = (ang - (s.lastAngle ?? ang)) * 60;
          s.lastAngle = ang;
          s.length = clamp(len, size.h*0.2, size.h*0.7);
        } else { s.av *= 0.98; }
        s.x = s.anchor.x + Math.sin(s.angle) * s.length;
        s.y = s.anchor.y + Math.cos(s.angle) * s.length;
      }
      // fade
      ctx.globalCompositeOperation = "source-over";
      ctx.fillStyle = `rgba(242,235,220, ${1 - trailDecay})`;
      ctx.fillRect(0, 0, size.w, size.h);
      // stamp
      ctx.globalCompositeOperation = "multiply";
      for (const s of arr) {
        const grd = ctx.createRadialGradient(s.x, s.y, 0, s.x, s.y, s.radius * 1.6);
        grd.addColorStop(0, s.color);
        grd.addColorStop(1, "rgba(0,0,0,0)");
        ctx.fillStyle = grd;
        ctx.beginPath();
        ctx.arc(s.x, s.y, s.radius * 1.6, 0, Math.PI * 2);
        ctx.fill();
      }
      ctx.globalCompositeOperation = "source-over";
      force((n) => (n + 1) % 1e6);
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [size.w, size.h, released, wild]);

  const onBallDown = (id, e) => {
    e.preventDefault(); e.stopPropagation();
    const r = stageRef.current.getBoundingClientRect();
    dragRef.current = { id, x: e.clientX - r.left, y: e.clientY - r.top };
    setReleased(false);
    const onMove = (ev) => {
      const rr = stageRef.current.getBoundingClientRect();
      dragRef.current = { id, x: ev.clientX - rr.left, y: ev.clientY - rr.top };
    };
    const onUp = () => {
      dragRef.current = null;
      setReleased(true);
      window.removeEventListener("mousemove", onMove);
      window.removeEventListener("mouseup", onUp);
    };
    window.addEventListener("mousemove", onMove);
    window.addEventListener("mouseup", onUp);
  };

  const wipe = () => {
    const c = canvasRef.current;
    const ctx = c.getContext("2d");
    ctx.fillStyle = "var(--paper)";
    ctx.fillRect(0, 0, size.w, size.h);
  };
  const stop = () => {
    const arr = swingsRef.current; if (!arr) return;
    for (const s of arr) { s.av = 0; s.angle = 0; }
  };
  const kick = () => {
    const arr = swingsRef.current; if (!arr) return;
    arr.forEach((s, i) => { s.av = (i % 2 ? 1 : -1) * (3 + Math.random()*2) * wild; });
  };

  const arr = swingsRef.current || [];

  return (
    <div ref={stageRef} style={{ position:"absolute", inset:0, overflow:"hidden" }}>
      <canvas ref={canvasRef} style={{ position:"absolute", inset:0, pointerEvents:"none" }} />

      {/* beam + posts */}
      {size.w > 0 && (
        <>
          <div style={{ position:"absolute", left:60, right:60, top:size.h*0.18 - 6, height:12, background:"var(--ink)", borderRadius:4 }} />
          <div style={{ position:"absolute", left:48, top:size.h*0.18 - 6, width:14, height:size.h*0.3, background:"var(--ink)" }} />
          <div style={{ position:"absolute", right:48, top:size.h*0.18 - 6, width:14, height:size.h*0.3, background:"var(--ink)" }} />
          <div style={{ position:"absolute", left:36, top:size.h*0.48 - 6, width:38, height:6, background:"var(--ink)" }} />
          <div style={{ position:"absolute", right:36, top:size.h*0.48 - 6, width:38, height:6, background:"var(--ink)" }} />
        </>
      )}

      {/* ropes */}
      {size.w > 0 && (
        <svg style={{ position:"absolute", inset:0, width:"100%", height:"100%", pointerEvents:"none" }}>
          {arr.map((s) => (
            <g key={s.id}>
              <line x1={s.anchor.x} y1={s.anchor.y} x2={s.x} y2={s.y}
                stroke="var(--ink)" strokeWidth="2" />
              <circle cx={s.anchor.x} cy={s.anchor.y} r="4" fill="var(--ink)" />
            </g>
          ))}
        </svg>
      )}

      {/* balls */}
      {arr.map((s) => (
        <div key={s.id} data-cursor="grab"
          onMouseDown={(e) => onBallDown(s.id, e)}
          style={{
            position:"absolute", left:s.x - s.radius, top:s.y - s.radius,
            width:s.radius*2, height:s.radius*2, borderRadius:"50%",
            background:s.color, border:"2px solid var(--ink)",
            boxShadow:"3px 3px 0 var(--ink)",
          }} />
      ))}

      {/* title */}
      <div style={{ position:"absolute", left:24, top:24, zIndex:10, maxWidth:280 }}>
        <div style={{ display:"flex", gap:8, marginBottom:10 }}>
          <Tag color="var(--sky)">03 / color</Tag>
          <Tag>painting</Tag>
        </div>
        <div style={{ fontFamily:"var(--display)", fontWeight:900, fontSize:68, lineHeight:.86, letterSpacing:"-.04em" }}>
          the<br/>
          <span style={{ fontStyle:"italic", fontFamily:"var(--serif)", fontWeight:400 }}>swings</span>.
        </div>
        <div style={{ marginTop:10, fontFamily:"var(--serif)", fontStyle:"italic", fontSize:18, color:"var(--ink-2)", lineHeight:1.3, maxWidth:260 }}>
          push a colour. let it cross another. paint the air between them.
        </div>
        <div style={{ marginTop:12, fontFamily:"var(--mono)", fontSize:11, letterSpacing:".06em", textTransform:"uppercase", display:"flex", flexDirection:"column", gap:5, opacity:.78 }}>
          <Hint k="drag">a ball to swing</Hint>
          <Hint k="kick">to push them all</Hint>
          <Hint k="wipe">to clear the canvas</Hint>
        </div>
      </div>

      {/* small action panel */}
      <div style={{ position:"absolute", right:24, top:24, zIndex:10, display:"flex", gap:6 }}
           onMouseDown={(e) => e.stopPropagation()}>
        <SandActionBtn onClick={kick} hue="var(--sun)">kick</SandActionBtn>
        <SandActionBtn onClick={stop} hue="var(--gum)">stop</SandActionBtn>
        <SandActionBtn onClick={wipe} hue="var(--paper)">wipe</SandActionBtn>
      </div>
    </div>
  );
}

Object.assign(window, { SlideZone, SandboxZone, SwingsZone });
