// ============================================================
// sandengine.jsx, a faithful, trimmed port of the real game's
// cellular-automaton physics + sprite renderer, packaged as a
// reusable <SandSim> React component.
// Ported from app_code/src/app/App.tsx.
// ============================================================
(function () {
  const { useRef, useEffect, useState, useCallback } = React;

  // ── Element ids (match the source) ──
  const EMPTY = 0, SAND = 1, WATER = 2, STONE = 3, FIRE = 4, SMOKE = 5,
        SEED = 6, SOIL = 7, PALM = 8, COCONUT = 9;

  const SAND_MAX_FALL = 3;
  const WATER_ANIM_SPEED = 14;
  const WATER_SPREAD = 3;
  const WATER_SOIL_SEEP = 0.12;
  const FIRE_LIFE = 120, SMOKE_LIFE = 60, IGNITE_PROB = 0.006;
  const COCONUT_REST_LIFE = 200;
  const PALM_MIN_CROWN = 5, PALM_WATER_RADIUS = 5;
  const GRID_C = 240;

  function gi(x, y, cols) { return y * cols + x; }
  function inB(x, y, cols, rows) { return x >= 0 && x < cols && y >= 0 && y < rows; }
  function swap(grid, meta, a, b) {
    const tg = grid[a]; grid[a] = grid[b]; grid[b] = tg;
    const tm = meta[a]; meta[a] = meta[b]; meta[b] = tm;
  }

  // ── Physics (ported) ──
  function updateSand(grid, meta, pr, idx, x, y, cols, rows) {
    if (y + 1 >= rows) return;
    let fall = 0;
    for (let d = 1; d <= SAND_MAX_FALL; d++) {
      if (y + d >= rows) break;
      const ni = gi(x, y + d, cols);
      if (grid[ni] === EMPTY) fall = d;
      else if (grid[ni] === WATER && d === 1) { fall = 1; break; }
      else break;
    }
    if (fall > 0) { const t = gi(x, y + fall, cols); swap(grid, meta, idx, t); pr[t] = 1; return; }
    const dirs = Math.random() < 0.5 ? [-1, 1] : [1, -1];
    for (const d of dirs) {
      const nx = x + d;
      if (!inB(nx, y + 1, cols, rows)) continue;
      const dg = gi(nx, y + 1, cols);
      if (grid[dg] === EMPTY || grid[dg] === WATER) { swap(grid, meta, idx, dg); pr[dg] = 1; return; }
    }
  }

  function updateWater(grid, meta, pr, idx, x, y, cols, rows) {
    if (y + 1 < rows) {
      const b = gi(x, y + 1, cols);
      if (grid[b] === EMPTY) { swap(grid, meta, idx, b); pr[b] = 1; return; }
      if (grid[b] === SOIL && Math.random() < WATER_SOIL_SEEP) { swap(grid, meta, idx, b); pr[b] = 1; return; }
    }
    const dirsD = Math.random() < 0.5 ? [-1, 1] : [1, -1];
    for (const d of dirsD) {
      if (!inB(x + d, y + 1, cols, rows)) continue;
      const dg = gi(x + d, y + 1, cols);
      if (grid[dg] === EMPTY) { swap(grid, meta, idx, dg); pr[dg] = 1; return; }
    }
    if (Math.random() < 0.7) {
      const dir = Math.random() < 0.5 ? -1 : 1;
      for (const sd of [dir, -dir]) {
        for (let s = 1; s <= WATER_SPREAD; s++) {
          const nx = x + sd * s;
          if (!inB(nx, y, cols, rows)) break;
          if (grid[gi(nx, y, cols)] !== EMPTY) break;
          const st = gi(x + sd, y, cols); swap(grid, meta, idx, st); pr[st] = 1; return;
        }
      }
    }
  }

  function updateFire(grid, meta, pr, idx, x, y, cols, rows) {
    if (meta[idx] > 0) meta[idx]--;
    const nb = [[0, -1], [0, 1], [-1, 0], [1, 0]];
    for (const [dx, dy] of nb) {
      const nx = x + dx, ny = y + dy;
      if (!inB(nx, ny, cols, rows)) continue;
      const ni = gi(nx, ny, cols);
      if (grid[ni] === WATER) { grid[idx] = SMOKE; meta[idx] = SMOKE_LIFE; grid[ni] = EMPTY; meta[ni] = 0; return; }
    }
    for (const [dx, dy] of nb) {
      const nx = x + dx, ny = y + dy;
      if (!inB(nx, ny, cols, rows)) continue;
      const ni = gi(nx, ny, cols);
      if (grid[ni] === SAND && Math.random() < IGNITE_PROB) { grid[ni] = FIRE; meta[ni] = FIRE_LIFE; }
    }
    if (meta[idx] === 0) { grid[idx] = SMOKE; meta[idx] = SMOKE_LIFE; return; }
    if (y > 0 && Math.random() < 0.4) {
      const dx = Math.floor(Math.random() * 3) - 1;
      const nx = Math.max(0, Math.min(cols - 1, x + dx));
      const ab = gi(nx, y - 1, cols);
      if (grid[ab] === EMPTY || grid[ab] === SMOKE) { swap(grid, meta, idx, ab); pr[ab] = 1; }
    }
  }

  function updateSmoke(grid, meta, pr, idx, x, y, cols, rows) {
    if (meta[idx] > 0) meta[idx]--;
    if (meta[idx] === 0) { grid[idx] = EMPTY; return; }
    if (y > 0 && Math.random() < 0.35) {
      const dx = Math.floor(Math.random() * 3) - 1;
      const nx = Math.max(0, Math.min(cols - 1, x + dx));
      const ab = gi(nx, y - 1, cols);
      if (grid[ab] === EMPTY) { swap(grid, meta, idx, ab); pr[ab] = 1; return; }
    }
    if (Math.random() < 0.2) {
      const dx = Math.random() < 0.5 ? -1 : 1, nx = x + dx;
      if (inB(nx, y, cols, rows)) { const ni = gi(nx, y, cols); if (grid[ni] === EMPTY) { swap(grid, meta, idx, ni); pr[ni] = 1; } }
    }
  }

  function updateSoil(grid, meta, pr, idx, x, y, cols, rows) {
    if (y + 1 >= rows) return;
    const b = gi(x, y + 1, cols);
    if (grid[b] === EMPTY || grid[b] === WATER) { swap(grid, meta, idx, b); pr[b] = 1; return; }
    if (Math.random() < 0.2) {
      const dirs = Math.random() < 0.5 ? [-1, 1] : [1, -1];
      for (const d of dirs) {
        const nx = x + d; if (!inB(nx, y + 1, cols, rows)) continue;
        const dg = gi(nx, y + 1, cols);
        if (grid[dg] === EMPTY || grid[dg] === WATER) { swap(grid, meta, idx, dg); pr[dg] = 1; return; }
      }
    }
  }

  function updateSeed(grid, meta, pr, idx, x, y, cols, rows) {
    if (y + 1 >= rows) return;
    const b = gi(x, y + 1, cols);
    if (grid[b] === EMPTY || grid[b] === WATER) {
      if (Math.random() < 0.3) { swap(grid, meta, idx, b); pr[b] = 1; }
      else if (Math.random() < 0.1) {
        const dx = Math.random() < 0.5 ? -1 : 1;
        if (inB(x + dx, y, cols, rows)) { const s = gi(x + dx, y, cols); if (grid[s] === EMPTY) { swap(grid, meta, idx, s); pr[s] = 1; } }
      }
      return;
    }
    let hasSoil = false, hasWater = false, wIdx = -1;
    const nbrs = [[-1, 1], [0, 1], [1, 1], [-1, 0], [1, 0], [-1, -1], [0, -1], [1, -1]];
    for (const [dx, dy] of nbrs) {
      const nx = x + dx, ny = y + dy; if (!inB(nx, ny, cols, rows)) continue;
      const t = grid[gi(nx, ny, cols)];
      if (t === SOIL) hasSoil = true;
      if (t === WATER && !hasWater) { hasWater = true; wIdx = gi(nx, ny, cols); }
    }
    if (hasSoil && hasWater && Math.random() < 0.06) {
      grid[wIdx] = EMPTY; meta[wIdx] = 0; grid[idx] = PALM; meta[idx] = 1; return;
    }
    if (Math.random() < 0.3) {
      const dirs = Math.random() < 0.5 ? [-1, 1] : [1, -1];
      for (const d of dirs) {
        if (!inB(x + d, y + 1, cols, rows)) continue;
        const dg = gi(x + d, y + 1, cols);
        if (grid[dg] === EMPTY || grid[dg] === WATER) { swap(grid, meta, idx, dg); pr[dg] = 1; return; }
      }
    }
  }

  function updatePalm(grid, meta, pr, idx, x, y, cols, rows) {
    const m = meta[idx];
    if (m >= 1 && m < 100) {
      let wc = 0;
      const wy0 = Math.max(0, y - PALM_WATER_RADIUS), wy1 = Math.min(rows - 1, y + PALM_WATER_RADIUS);
      const wx0 = Math.max(0, x - PALM_WATER_RADIUS), wx1 = Math.min(cols - 1, x + PALM_WATER_RADIUS);
      for (let wy = wy0; wy <= wy1; wy++) for (let wx = wx0; wx <= wx1; wx++) if (grid[gi(wx, wy, cols)] === WATER) wc++;
      const gp = wc === 0 ? 0 : Math.min(0.25, 0.03 + wc * 0.01);
      if (gp > 0 && y > 0 && Math.random() < gp) {
        const ab = gi(x, y - 1, cols);
        if (grid[ab] === EMPTY || grid[ab] === WATER) {
          grid[ab] = PALM; meta[ab] = Math.min(99, m + 1); meta[idx] = 100 + Math.min(98, m); pr[ab] = 1;
        }
      }
      if (m >= PALM_MIN_CROWN && Math.random() < 0.012) {
        const side = Math.random() < 0.5 ? -1 : 1;
        const cdx = side * (2 + Math.floor(Math.random() * 2));
        const cdy = Math.random() < 0.5 ? 0 : 1;
        const tx = x + cdx, ty = y + cdy;
        if (inB(tx, ty, cols, rows)) { const dt = gi(tx, ty, cols); if (grid[dt] === EMPTY) { grid[dt] = COCONUT; meta[dt] = Math.floor(Math.random() * 3); } }
      }
    }
  }

  function updateCoconut(grid, meta, dmg, pr, idx, x, y, cols, rows) {
    if (y + 1 >= rows) { dmg[idx]++; if (dmg[idx] >= COCONUT_REST_LIFE) { grid[idx] = EMPTY; meta[idx] = 0; dmg[idx] = 0; } return; }
    const b = gi(x, y + 1, cols);
    if (grid[b] === EMPTY || grid[b] === WATER) { dmg[idx] = 0; swap(grid, meta, idx, b); pr[b] = 1; return; }
    const dirs = Math.random() < 0.5 ? [-1, 1] : [1, -1];
    for (const d of dirs) {
      const nx = x + d; if (!inB(nx, y + 1, cols, rows)) continue;
      const dg = gi(nx, y + 1, cols);
      if (grid[dg] === EMPTY || grid[dg] === WATER) { dmg[idx] = 0; swap(grid, meta, idx, dg); pr[dg] = 1; return; }
    }
    dmg[idx]++; if (dmg[idx] >= COCONUT_REST_LIFE) { grid[idx] = EMPTY; meta[idx] = 0; dmg[idx] = 0; }
  }

  function step(grid, meta, dmg, pr, cols, rows, frame) {
    pr.fill(0);
    const ltr = frame % 2 === 0;
    for (let y = rows - 1; y >= 0; y--) {
      const xs = ltr ? range(0, cols, 1) : range(cols - 1, -1, -1);
      for (const x of xs) {
        const idx = gi(x, y, cols);
        if (pr[idx]) continue;
        const t = grid[idx];
        if (t === EMPTY || t === STONE) continue;
        pr[idx] = 1;
        if (t === SAND) updateSand(grid, meta, pr, idx, x, y, cols, rows);
        else if (t === WATER) updateWater(grid, meta, pr, idx, x, y, cols, rows);
        else if (t === FIRE) updateFire(grid, meta, pr, idx, x, y, cols, rows);
        else if (t === SMOKE) updateSmoke(grid, meta, pr, idx, x, y, cols, rows);
        else if (t === SOIL) updateSoil(grid, meta, pr, idx, x, y, cols, rows);
        else if (t === SEED) updateSeed(grid, meta, pr, idx, x, y, cols, rows);
        else if (t === PALM) updatePalm(grid, meta, pr, idx, x, y, cols, rows);
        else if (t === COCONUT) updateCoconut(grid, meta, dmg, pr, idx, x, y, cols, rows);
      }
    }
  }
  function range(a, b, s) { const o = []; for (let i = a; s > 0 ? i < b : i > b; i += s) o.push(i); return o; }

  // ── Sprite cache: rasterize SVGs at a given CELL into ImageData ──
  const IMG = {};
  const SPRITE_SRC = {
    sand: ['assets/pixels/sand0.svg', 'assets/pixels/sand1.svg', 'assets/pixels/sand2.svg'],
    water: ['assets/pixels/water0.svg', 'assets/pixels/water1.svg', 'assets/pixels/water2.svg'],
    fire: ['assets/pixels/fire.svg'],
    stone: ['assets/pixels/stone0.svg', 'assets/pixels/stone1.svg', 'assets/pixels/stone2.svg'],
    soil: ['assets/pixels/soil0.svg', 'assets/pixels/soil1.svg', 'assets/pixels/soil2.svg'],
    seed: ['assets/pixels/seed0.svg', 'assets/pixels/seed1.svg', 'assets/pixels/seed2.svg'],
    co: ['assets/pixels/co.svg'],
    coconut: ['assets/pixels/coconut0.svg', 'assets/pixels/coconut1.svg', 'assets/pixels/coconut2.svg'],
    stem: ['assets/icons/stem0.svg', 'assets/icons/stem1.svg', 'assets/icons/stem2.svg'],
    stemGreen: ['assets/icons/stem_green_0.svg', 'assets/icons/stem_green_1.svg', 'assets/icons/stem_green_2.svg'],
  };
  const crownImg = new Image(); crownImg.src = 'assets/icons/crown.svg';

  function loadImages() {
    const all = [];
    for (const k in SPRITE_SRC) { IMG[k] = []; SPRITE_SRC[k].forEach((src, i) => { const im = new Image(); im.src = src; IMG[k][i] = im; all.push(im); }); }
    return Promise.all(all.map(im => im.complete ? Promise.resolve() : new Promise(r => { im.onload = r; im.onerror = r; })));
  }
  const imagesReady = loadImages();

  const spriteCache = {}; // key: `${kind}${i}@${cell}` -> Uint8ClampedArray
  function getSprite(kind, i, cell) {
    const key = `${kind}${i}@${cell}`;
    if (spriteCache[key]) return spriteCache[key];
    const im = IMG[kind] && IMG[kind][i];
    if (!im || !im.complete || im.naturalWidth === 0) return null;
    const oc = document.createElement('canvas'); oc.width = cell; oc.height = cell;
    const octx = oc.getContext('2d'); octx.clearRect(0, 0, cell, cell);
    octx.drawImage(im, 0, 0, cell, cell);
    const data = octx.getImageData(0, 0, cell, cell).data;
    spriteCache[key] = data; return data;
  }

  // ── Renderer (per-pixel ImageData, ported + trimmed) ──
  function renderGrid(ctx, grid, meta, cols, rows, cw, ch, cell, frame, hover, dark) {
    const cImg = ctx.createImageData(cw, ch);
    const data = cImg.data;
    const emptyFill = dark ? 30 : 255;
    const gridLine = dark ? 64 : GRID_C;
    const bgFill = dark ? 30 : 255;
    const dCols = Math.floor(cw / cell), dRows = Math.floor(ch / cell);

    for (let dy = 0; dy < dRows; dy++) {
      for (let dx = 0; dx < dCols; dx++) {
        if (dx >= cols || dy >= rows) continue;
        const wi = gi(dx, dy, cols), type = grid[wi], m = meta[wi];
        const px0 = dx * cell, py0 = dy * cell;
        const hovered = hover && dx === hover.x && dy === hover.y;

        if (type === SAND || type === WATER || type === FIRE || type === STONE || type === SEED || type === SOIL || type === PALM || type === COCONUT) {
          let kind, vi = m % 3;
          if (type === SAND) kind = 'sand';
          else if (type === WATER) { kind = 'water'; vi = (Math.floor(frame / WATER_ANIM_SPEED) + m) % 3; }
          else if (type === FIRE) { kind = 'fire'; vi = 0; }
          else if (type === STONE) kind = 'stone';
          else if (type === SEED) kind = 'seed';
          else if (type === SOIL) kind = 'soil';
          else if (type === COCONUT) { kind = 'coconut'; vi = (dx * 73 + dy * 19) % 3; }
          else if (type === PALM) { const h = m >= 100 && m < 200 ? m - 100 : m; kind = h >= 7 ? 'stemGreen' : 'stem'; vi = (dx * 73 + dy * 19) % 3; }
          const sprite = getSprite(kind, vi, cell);
          if (!sprite) continue;
          const life = type === FIRE ? Math.max(0.2, m / FIRE_LIFE) : 1.0;
          let wet = false;
          if (type === SAND || type === SOIL) {
            wet = (dx > 0 && grid[gi(dx - 1, dy, cols)] === WATER) || (dx < cols - 1 && grid[gi(dx + 1, dy, cols)] === WATER) ||
                  (dy > 0 && grid[gi(dx, dy - 1, cols)] === WATER) || (dy < rows - 1 && grid[gi(dx, dy + 1, cols)] === WATER);
          }
          for (let cy = 0; cy < cell; cy++) {
            const py = py0 + cy; if (py >= ch) break;
            for (let cx = 0; cx < cell; cx++) {
              const px = px0 + cx; if (px >= cw) break;
              const si = (cy * cell + cx) * 4;
              const isB = cx === 0 || cy === 0 || cx === cell - 1 || cy === cell - 1;
              const f = isB ? 0.75 : life;
              let r = sprite[si] * f, g = sprite[si + 1] * f, b = sprite[si + 2] * f;
              if (wet) { r *= 0.62; g *= 0.70; b *= 0.82; }
              if (type === COCONUT) { const a = sprite[si + 3] / 255; r = r * a + bgFill * (1 - a); g = g * a + bgFill * (1 - a); b = b * a + bgFill * (1 - a); }
              if (hovered) { r *= 0.6; g *= 0.6; b *= 0.6; }
              const pi = (py * cw + px) * 4;
              data[pi] = r; data[pi + 1] = g; data[pi + 2] = b; data[pi + 3] = 255;
            }
          }
          continue;
        }

        // flat cells: empty / smoke
        let r = emptyFill, g = emptyFill, b = emptyFill;
        if (type === SMOKE) { const a = m / SMOKE_LIFE; const sg = Math.round(120 * a + (dark ? 30 : 255) * (1 - a)); r = g = b = sg; }
        for (let cy = 0; cy < cell; cy++) {
          const py = py0 + cy; if (py >= ch) break;
          for (let cx = 0; cx < cell; cx++) {
            const px = px0 + cx; if (px >= cw) break;
            let pr = r, pg = g, pb = b;
            const isB = cx === 0 || cy === 0 || cx === cell - 1 || cy === cell - 1;
            if (isB) {
              if (type === EMPTY) { if (hovered) { pr = pg = pb = dark ? 98 : 213; } else { pr = pg = pb = gridLine; } }
              else { pr *= 0.88; pg *= 0.88; pb *= 0.88; }
            } else if (hovered && type === EMPTY) { pr = pg = pb = dark ? 60 : 235; }
            const pi = (py * cw + px) * 4;
            data[pi] = pr; data[pi + 1] = pg; data[pi + 2] = pb; data[pi + 3] = 255;
          }
        }
      }
    }
    ctx.putImageData(cImg, 0, 0);

    // crown overlay
    if (crownImg.complete && crownImg.naturalWidth) {
      for (let dy = 0; dy < dRows; dy++) for (let dx = 0; dx < dCols; dx++) {
        if (dx >= cols || dy >= rows) continue;
        const wi = gi(dx, dy, cols); if (grid[wi] !== PALM) continue;
        const isTop = dy === 0 || grid[gi(dx, dy - 1, cols)] !== PALM; if (!isTop) continue;
        const tm = meta[wi]; let sc = 0;
        if (tm >= 1 && tm < 100) sc = tm < PALM_MIN_CROWN ? 0 : Math.min(1, (tm - PALM_MIN_CROWN + 1) / 8);
        if (sc <= 0) continue;
        ctx.save(); ctx.translate(dx * cell + cell / 2, dy * cell + cell); ctx.scale(sc, sc);
        ctx.drawImage(crownImg, -3.5 * cell, -3 * cell, 7 * cell, 4 * cell); ctx.restore();
      }
    }
  }

  // ── SandSim component ──
  // props: tools[], defaultTool, cell, height, dark, seed(fn(api)), showToolbar, instructions
  function SandSim(props) {
    const { tools = ['sand', 'water', 'stone', 'fire', 'soil', 'seed', 'wipe'],
            defaultTool = 'sand', cell = 8, height = 360, dark = false,
            seed = null, showToolbar = true, brush = 2, paused = false } = props;
    const wrapRef = useRef(null);
    const canvasRef = useRef(null);
    const sim = useRef(null);
    const [tool, setTool] = useState(defaultTool);
    const [ready, setReady] = useState(false);
    const [pressed, setPressed] = useState(null);
    const toolRef = useRef(tool); toolRef.current = tool;
    const hoverRef = useRef(null);
    const paintRef = useRef(false);
    const darkRef = useRef(dark); darkRef.current = dark;
    const pausedRef = useRef(paused); pausedRef.current = paused;

    const toolToType = { sand: SAND, water: WATER, stone: STONE, fire: FIRE, soil: SOIL, seed: SEED, wipe: EMPTY };

    const paintAt = useCallback((cx, cy) => {
      const s = sim.current; if (!s) return;
      const t = toolToType[toolRef.current];
      const r = brush;
      for (let dy = -r; dy <= r; dy++) for (let dx = -r; dx <= r; dx++) {
        if (dx * dx + dy * dy > r * r + 1) continue;
        const x = cx + dx, y = cy + dy;
        if (!inB(x, y, s.cols, s.rows)) continue;
        const idx = gi(x, y, s.cols);
        const curr = s.grid[idx];
        if (toolRef.current === 'sand' && curr === SOIL) continue;
        if (toolRef.current === 'soil' && curr === SAND) continue;
        if ((toolRef.current === 'sand' || toolRef.current === 'soil') && curr === SEED) continue;
        if (t === EMPTY) { s.grid[idx] = EMPTY; s.meta[idx] = 0; s.dmg[idx] = 0; }
        else if (t === FIRE) { s.grid[idx] = FIRE; s.meta[idx] = FIRE_LIFE; }
        else { s.grid[idx] = t; s.meta[idx] = t === SAND || t === WATER || t === STONE || t === SOIL || t === SEED ? Math.floor(Math.random() * 3) : 0; }
      }
    }, [brush]);

    useEffect(() => {
      let raf, frame = 0, mounted = true;
      imagesReady.then(() => {
        if (!mounted) return;
        const wrap = wrapRef.current, cv = canvasRef.current;
        if (!wrap || !cv) return;
        let cw = 0, chh = 0, ctx = null;
        const build = () => {
          cw = Math.floor(wrap.clientWidth / cell) * cell;
          chh = Math.floor(height / cell) * cell;
          if (cw <= 0 || chh <= 0) return false;
          cv.width = cw; cv.height = chh;
          const cols = Math.floor(cw / cell), rows = Math.floor(chh / cell);
          sim.current = { grid: new Uint8Array(cols * rows), meta: new Uint8Array(cols * rows), dmg: new Uint16Array(cols * rows), pr: new Uint8Array(cols * rows), cols, rows };
          if (seed) seed({ set: (x, y, type, m = 0) => { if (inB(x, y, cols, rows)) { sim.current.grid[gi(x, y, cols)] = type; sim.current.meta[gi(x, y, cols)] = m; } }, cols, rows, T: { SAND, WATER, STONE, FIRE, SOIL, SEED, PALM, COCONUT } });
          ctx = cv.getContext('2d');
          setReady(true);
          return true;
        };
        const loop = () => {
          if (!mounted) return;
          // Defer init until the container has a real width (fonts/layout settle).
          // Never throw on a transient zero size — just retry next frame.
          if (!ctx || cw <= 0 || chh <= 0) {
            build();
            raf = requestAnimationFrame(loop);
            return;
          }
          const s = sim.current;
          if (!pausedRef.current) { step(s.grid, s.meta, s.dmg, s.pr, s.cols, s.rows, frame); frame++; }
          renderGrid(ctx, s.grid, s.meta, s.cols, s.rows, cw, chh, cell, frame, hoverRef.current, darkRef.current);
          raf = requestAnimationFrame(loop);
        };
        loop();
      });
      return () => { mounted = false; cancelAnimationFrame(raf); };
    }, [cell, height]);

    const evtCell = (e) => {
      const cv = canvasRef.current, rect = cv.getBoundingClientRect();
      const sx = cv.width / rect.width, sy = cv.height / rect.height;
      const x = Math.floor((e.clientX - rect.left) * sx / cell);
      const y = Math.floor((e.clientY - rect.top) * sy / cell);
      return { x, y };
    };

    return (
      React.createElement('div', { ref: wrapRef, style: { width: '100%' } },
        showToolbar && React.createElement(MiniToolbar, { tools, tool, setTool, dark, pressed, setPressed,
          onClear: () => { const s = sim.current; if (s) { s.grid.fill(0); s.meta.fill(0); s.dmg.fill(0); } } }),
        React.createElement('canvas', {
          ref: canvasRef,
          style: { display: 'block', width: '100%', imageRendering: 'pixelated', cursor: 'crosshair',
            background: darkRef.current ? '#1E1E1E' : '#fff', touchAction: 'none',
            borderRadius: showToolbar ? '0 0 14px 14px' : '14px', border: `1px solid var(--border)`,
            borderTop: showToolbar ? 'none' : `1px solid var(--border)` },
          onPointerDown: (e) => { e.currentTarget.setPointerCapture(e.pointerId); paintRef.current = true; const c = evtCell(e); hoverRef.current = c; paintAt(c.x, c.y); },
          onPointerMove: (e) => { const c = evtCell(e); hoverRef.current = c; if (paintRef.current) paintAt(c.x, c.y); },
          onPointerUp: () => { paintRef.current = false; },
          onPointerLeave: () => { paintRef.current = false; hoverRef.current = null; },
        })
      )
    );
  }

  // Mini toolbar that reuses the REAL game icon SVGs
  function MiniToolbar({ tools, tool, setTool, dark, pressed, setPressed, onClear }) {
    const iconFor = (t) => {
      const state = pressed === t ? 'pressed' : tool === t ? 'selected' : 'default';
      return `assets/icons/${t}-${dark ? 'dark-' : ''}${state}.svg`;
    };
    return React.createElement('div', {
      style: { display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap', padding: '10px 12px',
        background: dark ? '#1E1E1E' : '#fff', border: `1px solid var(--border)`, borderBottom: 'none',
        borderRadius: '14px 14px 0 0' }
    },
      tools.map(t => React.createElement('button', {
        key: t, title: t,
        onPointerDown: () => { setPressed(t); },
        onPointerUp: () => { setPressed(null); setTool(t); },
        onPointerLeave: () => setPressed(null),
        style: { background: 'none', border: 'none', padding: 0, cursor: 'pointer', lineHeight: 0 }
      }, React.createElement('img', { src: iconFor(t), width: 52, height: 52, alt: t, draggable: false, style: { display: 'block' } }))),
      React.createElement('div', { style: { flex: 1 } }),
      React.createElement('button', {
        onClick: onClear,
        style: { fontFamily: 'var(--font-pixel)', fontSize: 22, lineHeight: 1, padding: '6px 16px', cursor: 'pointer',
          background: dark ? '#272727' : '#E6E6E6', color: dark ? '#ddd' : '#333', border: 'none', borderRadius: 6 }
      }, 'Clear')
    );
  }

  window.SandSim = SandSim;
  window.SAND_T = { EMPTY, SAND, WATER, STONE, FIRE, SMOKE, SEED, SOIL, PALM, COCONUT };
})();
