{"$schema":"https://ui.shadcn.com/schema/registry.json","author":"Kaiyu Hsu <uicapsule@kyh.io>","categories":["pages","pixel-art"],"dependencies":[],"description":"Isometric voxel diorama landing page — scroll pans diagonally through a living world of walkers, marquee signs, and ground-laid typography","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { VoxelLanding } from \"./voxel-landing\";\n\nconst Preview = () => (\n  <main className=\"h-dvh w-full bg-[#0f1114]\">\n    <VoxelLanding />\n  </main>\n);\n\nexport default Preview;\n","path":"/preview.tsx","target":"uicapsule/voxel-landing/preview.tsx","type":"registry:file"},{"content":"\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\n\n// 5x5 pixel font — only the glyphs the sections need.\nconst FONT = new Map(\n  Object.entries({\n    A: [\".###.\", \"#...#\", \"#####\", \"#...#\", \"#...#\"],\n    B: [\"####.\", \"#...#\", \"####.\", \"#...#\", \"####.\"],\n    C: [\".####\", \"#....\", \"#....\", \"#....\", \".####\"],\n    D: [\"####.\", \"#...#\", \"#...#\", \"#...#\", \"####.\"],\n    E: [\"#####\", \"#....\", \"####.\", \"#....\", \"#####\"],\n    H: [\"#...#\", \"#...#\", \"#####\", \"#...#\", \"#...#\"],\n    I: [\"#####\", \"..#..\", \"..#..\", \"..#..\", \"#####\"],\n    K: [\"#...#\", \"#..#.\", \"###..\", \"#..#.\", \"#...#\"],\n    L: [\"#....\", \"#....\", \"#....\", \"#....\", \"#####\"],\n    O: [\".###.\", \"#...#\", \"#...#\", \"#...#\", \".###.\"],\n    P: [\"####.\", \"#...#\", \"####.\", \"#....\", \"#....\"],\n    R: [\"####.\", \"#...#\", \"####.\", \"#..#.\", \"#...#\"],\n    S: [\".####\", \"#....\", \".###.\", \"....#\", \"####.\"],\n    T: [\"#####\", \"..#..\", \"..#..\", \"..#..\", \"..#..\"],\n    U: [\"#...#\", \"#...#\", \"#...#\", \"#...#\", \".###.\"],\n    V: [\"#...#\", \"#...#\", \"#...#\", \".#.#.\", \"..#..\"],\n    X: [\"#...#\", \".#.#.\", \"..#..\", \".#.#.\", \"#...#\"],\n  }),\n);\n\nconst ACCENTS = [\"#e23d2e\", \"#f0b429\", \"#3fa945\", \"#3186d4\"] as const;\nconst accentAt = (i: number) => ACCENTS[i % ACCENTS.length] ?? ACCENTS[0];\n\n// The corridor runs along +z; scrolling pans the camera so the world\n// slides up-right on screen (matching classic iso site scroll).\nconst SECTION_GAP = 60;\nconst SECTIONS = [\n  { accent: ACCENTS[0], word: \"VOXEL\" },\n  { accent: ACCENTS[1], word: \"BUILD\" },\n  { accent: ACCENTS[2], word: \"STACK\" },\n  { accent: ACCENTS[3], word: \"SHIP\" },\n  { accent: ACCENTS[0], word: \"START\" },\n] as const;\nconst WORLD_LEN = SECTION_GAP * (SECTIONS.length - 1);\n\n// Iso half-width of a unit cube, in px.\nconst TILE = 13;\nconst HALF = TILE * 0.5;\nconst GRASS = [\"#2c7a33\", \"#3fa945\", \"#57b85e\"] as const;\nconst grassAt = (i: number) => GRASS[i % GRASS.length] ?? GRASS[0];\n\n// Deterministic randomness — every load (and recording) is identical.\nconst hash = (n: number) => {\n  const s = Math.sin(n * 127.1 + 311.7) * 43_758.5453;\n  return s - Math.floor(s);\n};\n\nconst shadeCache = new Map<string, string>();\nconst shade = (hex: string, factor: number) => {\n  const key = `${hex}:${factor}`;\n  const cached = shadeCache.get(key);\n  if (cached) {\n    return cached;\n  }\n  const r = Math.round(Number.parseInt(hex.slice(1, 3), 16) * factor);\n  const g = Math.round(Number.parseInt(hex.slice(3, 5), 16) * factor);\n  const b = Math.round(Number.parseInt(hex.slice(5, 7), 16) * factor);\n  const out = `rgb(${r},${g},${b})`;\n  shadeCache.set(key, out);\n  return out;\n};\n\ntype Kind = \"static\" | \"sign\" | \"road\";\n\n// x runs across the corridor, y up (in tiles), z along it — the camera travels +z.\n// size scales horizontally (1 = full tile), h vertically; section tags sign marquee cells.\ninterface Voxel {\n  x: number;\n  y: number;\n  z: number;\n  size: number;\n  h: number;\n  color: string;\n  alpha: number;\n  kind: Kind;\n  section: number;\n  seed: number;\n}\n\nconst vox = (partial: Partial<Voxel> & Pick<Voxel, \"x\" | \"y\" | \"z\" | \"color\">): Voxel => ({\n  alpha: 1,\n  h: 1,\n  kind: \"static\",\n  section: -1,\n  seed: 0,\n  size: 1,\n  ...partial,\n});\n\n// Upright marquee sign: hollow dark frame + floating glyph cells, spanning x.\nconst buildSign = (out: Voxel[], word: string, cz: number, section: number) => {\n  const cols = word.length * 6 - 1;\n  const half = Math.floor(cols / 2) + 2;\n  for (let x = -half; x <= half; x += 1) {\n    out.push(vox({ color: \"#33363b\", x, y: 1, z: cz }), vox({ color: \"#33363b\", x, y: 9, z: cz }));\n  }\n  for (let y = 2; y <= 8; y += 1) {\n    out.push(\n      vox({ color: \"#33363b\", x: -half, y, z: cz }),\n      vox({ color: \"#33363b\", x: half, y, z: cz }),\n    );\n  }\n  for (const [li, ch] of [...word].entries()) {\n    const glyph = FONT.get(ch) ?? [];\n    for (const [gy, row] of glyph.entries()) {\n      for (const [gx, cell] of [...row].entries()) {\n        if (cell !== \"#\") {\n          continue;\n        }\n        const x = li * 6 + gx - cols / 2;\n        out.push(\n          vox({ color: \"#62666c\", kind: \"sign\", section, seed: out.length, x, y: 7 - gy, z: cz }),\n        );\n      }\n    }\n  }\n};\n\n// Word laid flat on the ground plane, glyphs running along x, rows along z.\nconst buildFlatWord = (out: Voxel[], word: string, cx: number, cz: number, accent: string) => {\n  const cols = word.length * 6 - 1;\n  for (const [li, ch] of [...word].entries()) {\n    const glyph = FONT.get(ch) ?? [];\n    for (const [gz, row] of glyph.entries()) {\n      for (const [gx, cell] of [...row].entries()) {\n        if (cell !== \"#\") {\n          continue;\n        }\n        const seed = out.length;\n        out.push(\n          vox({\n            color: hash(seed * 3.7) < 0.09 ? accent : \"#8b9096\",\n            h: 0.35,\n            seed,\n            x: cx + li * 6 + gx - cols / 2,\n            y: 0,\n            z: cz + gz,\n          }),\n        );\n      }\n    }\n  }\n};\n\n// Road centerline; movers follow it smoothly (the drawn tiles are rounded).\nconst roadX = (z: number) => Math.sin(z * 0.16) * 4 + 4;\n\n// Torii-style gate straddling the road.\nconst buildGate = (out: Voxel[], z: number, color: string) => {\n  const cx = Math.round(roadX(z));\n  for (let y = 0; y < 4; y += 1) {\n    out.push(vox({ color, x: cx - 4, y, z }), vox({ color, x: cx + 4, y, z }));\n  }\n  for (let x = cx - 5; x <= cx + 5; x += 1) {\n    out.push(vox({ color, x, y: 4, z }));\n  }\n  out.push(vox({ color: \"#33363b\", size: 0.5, x: cx, y: 5, z }));\n};\n\n// Fountain basin — the jets are animated per-frame.\nconst FOUNTAIN = { x: -9, z: 30 };\nconst buildFountain = (out: Voxel[]) => {\n  for (let dx = -4; dx <= 4; dx += 1) {\n    for (let dz = -4; dz <= 4; dz += 1) {\n      const r = Math.hypot(dx, dz);\n      if (r > 4.2) {\n        continue;\n      }\n      const seed = out.length;\n      if (r > 3.1) {\n        out.push(\n          vox({ color: \"#c9ced3\", h: 0.3, seed, x: FOUNTAIN.x + dx, y: 0, z: FOUNTAIN.z + dz }),\n        );\n      } else {\n        out.push(\n          vox({\n            color: hash(seed * 2.1) < 0.15 ? \"#4a9de0\" : \"#3186d4\",\n            h: 0.18,\n            seed,\n            x: FOUNTAIN.x + dx,\n            y: 0,\n            z: FOUNTAIN.z + dz,\n          }),\n        );\n      }\n    }\n  }\n  out.push(vox({ color: \"#9aa0a6\", size: 0.7, x: FOUNTAIN.x, y: 0, z: FOUNTAIN.z }));\n};\n\nconst buildTree = (out: Voxel[], x: number, z: number, seed: number) => {\n  out.push(\n    vox({ color: \"#7a5a3a\", size: 0.5, x, y: 0, z }),\n    vox({ color: \"#7a5a3a\", size: 0.5, x, y: 1, z }),\n  );\n  const canopy: [number, number, number][] = [\n    [0, 2, 0],\n    [1, 2, 0],\n    [-1, 2, 0],\n    [0, 2, 1],\n    [0, 2, -1],\n    [0, 3, 0],\n  ];\n  for (const [i, [dx, dy, dz]] of canopy.entries()) {\n    out.push(vox({ color: grassAt(i + seed), x: x + dx, y: dy, z: z + dz }));\n  }\n};\n\nconst buildWorld = (): Voxel[] => {\n  const out: Voxel[] = [];\n\n  buildSign(out, \"VOXEL\", -8, 0);\n  buildSign(out, \"START\", WORLD_LEN - 8, 4);\n\n  buildFountain(out);\n  buildGate(out, 45, \"#e23d2e\");\n  buildGate(out, 150, \"#f0b429\");\n  buildGate(out, 212, \"#3fa945\");\n\n  buildFlatWord(out, \"BUILD\", -10, SECTION_GAP + 9, ACCENTS[1]);\n  buildFlatWord(out, \"STACK\", -10, SECTION_GAP * 2 + 9, ACCENTS[2]);\n  buildFlatWord(out, \"SHIP\", -10, SECTION_GAP * 3 + 9, ACCENTS[3]);\n\n  // BUILD: a grove flanking the word.\n  buildTree(out, 13, SECTION_GAP - 12, 0);\n  buildTree(out, 15, SECTION_GAP - 2, 1);\n  buildTree(out, 12, SECTION_GAP + 9, 2);\n  buildTree(out, 16, SECTION_GAP + 16, 0);\n  buildTree(out, -16, SECTION_GAP + 2, 1);\n\n  // STACK: a bar-chart skyline of cube towers.\n  for (let i = 0; i < 12; i += 1) {\n    const tz = SECTION_GAP * 2 + Math.floor((hash(i * 7.9) - 0.5) * 30);\n    const tx = 10 + Math.floor(hash(i * 3.3) * 8);\n    const height = 2 + Math.floor(hash(i * 5.1) * 5);\n    for (let y = 0; y < height - 1; y += 1) {\n      out.push(vox({ color: y % 2 === 0 ? \"#5a5f66\" : \"#6b7076\", x: tx, y, z: tz }));\n    }\n    out.push(vox({ color: accentAt(i), x: tx, y: height - 1, z: tz }));\n  }\n\n  // SHIP: pond of flat water tiles beside the word.\n  for (let dz = -8; dz <= 8; dz += 1) {\n    for (let dx = -5; dx <= 5; dx += 1) {\n      if ((dz / 8) ** 2 + (dx / 5) ** 2 > 1) {\n        continue;\n      }\n      const seed = out.length;\n      out.push(\n        vox({\n          color: hash(seed * 2.1) < 0.15 ? \"#4a9de0\" : \"#3186d4\",\n          h: 0.18,\n          seed,\n          x: 12 + dx,\n          y: 0,\n          z: SECTION_GAP * 3 + dz,\n        }),\n      );\n    }\n  }\n\n  // Winding dash-road tying the whole strip together.\n  for (let z = -14; z <= WORLD_LEN + 14; z += 1) {\n    if (hash(z * 9.7) < 0.3) {\n      continue;\n    }\n    const x = Math.round(roadX(z));\n    out.push(vox({ color: \"#4a9de0\", h: 0.12, kind: \"road\", seed: z, size: 0.6, x, y: 0, z }));\n  }\n\n  // Flag poles along the road.\n  for (let i = 0; i < 14; i += 1) {\n    const pz = Math.floor(hash(i * 13.7) * (WORLD_LEN + 20)) - 10;\n    const px = Math.round(Math.sin(pz * 0.16) * 4) + 4 + (hash(i * 3.9) < 0.5 ? 3 : -3);\n    out.push(\n      vox({ color: \"#7d838a\", h: 3, size: 0.22, x: px, y: 0, z: pz }),\n      vox({ color: accentAt(i), size: 0.5, x: px, y: 3, z: pz }),\n    );\n  }\n\n  // Flora scattered across the whole corridor.\n  for (let i = 0; i < 260; i += 1) {\n    const fz = Math.floor(hash(i * 17.3) * (WORLD_LEN + 36)) - 18;\n    const fx = Math.floor((hash(i * 23.1) - 0.5) * 34);\n    const roll = hash(i * 5.9);\n    if (roll < 0.72) {\n      out.push(vox({ color: grassAt(i), h: 0.5, size: 0.4, x: fx, y: 0, z: fz }));\n    } else {\n      out.push(\n        vox({ color: \"#3fa945\", h: 1, size: 0.16, x: fx, y: 0, z: fz }),\n        vox({ color: accentAt(i), size: 0.34, x: fx, y: 1, z: fz }),\n      );\n    }\n  }\n\n  return out;\n};\n\n// Walkers wander around a home point; position is a pure function of time.\ninterface Walker {\n  home: [number, number];\n  radius: number;\n  body: string;\n  id: number;\n}\n\nconst buildWalkers = (): Walker[] => {\n  const walkers: Walker[] = [];\n  const add = (hx: number, hz: number, radius: number) => {\n    walkers.push({\n      body: accentAt(walkers.length),\n      home: [hx, hz],\n      id: walkers.length,\n      radius,\n    });\n  };\n  for (let i = 0; i < 5; i += 1) {\n    add(4 + i * 2, Math.floor(hash(i * 3.1) * 24) - 12, 5);\n  }\n  add(2, SECTION_GAP - 8, 6);\n  add(16, SECTION_GAP + 6, 6);\n  add(3, SECTION_GAP * 2 + 4, 6);\n  add(17, SECTION_GAP * 2 - 10, 5);\n  add(2, SECTION_GAP * 3 - 6, 5);\n  add(16, SECTION_GAP * 3 + 8, 5);\n  for (let i = 0; i < 9; i += 1) {\n    add(2 + Math.floor(hash(i * 4.3) * 10), WORLD_LEN + Math.floor(hash(i * 7.7) * 22) - 11, 4);\n  }\n  return walkers;\n};\n\nconst walkerPos = (w: Walker, t: number): [number, number, boolean] => {\n  const epoch = Math.floor(t / 2.6 + hash(w.id * 31.7));\n  const frac = (t / 2.6 + hash(w.id * 31.7)) % 1;\n  const wp = (k: number): [number, number] => [\n    w.home[0] + (hash(w.id * 91.3 + k * 17.1) - 0.5) * 2 * w.radius,\n    w.home[1] + (hash(w.id * 47.9 + k * 29.3) - 0.5) * 2 * w.radius,\n  ];\n  const [ax, az] = wp(epoch);\n  const [bx, bz] = wp(epoch + 1);\n  // Walk, then idle at the waypoint.\n  const ease = frac < 0.7 ? frac / 0.7 : 1;\n  const x = Math.round((ax + (bx - ax) * ease) * 2) / 2;\n  const z = Math.round((az + (bz - az) * ease) * 2) / 2;\n  return [x, z, frac < 0.7];\n};\n\n// HTML copy laid into the iso ground plane, y-n10 style.\n// matrix(1,.5,-1,.5): css X → world +x (down-right), css Y → world +z.\n// matrix(1,-.5,1,.5): text running up-right along world −z, for the hero.\nconst GROUND = \"matrix(1, 0.5, -1, 0.5, 0, 0)\";\nconst GROUND_CROSS = \"matrix(1, -0.5, 1, 0.5, 0, 0)\";\n\ninterface Label {\n  x: number;\n  z: number;\n  matrix: string;\n  lines: [string, string];\n  size: string;\n}\n\nconst LABELS: Label[] = [\n  {\n    lines: [\"Voxelworks\", \"A tiny nation of cubes\"],\n    matrix: GROUND_CROSS,\n    size: \"text-xl\",\n    x: 14,\n    z: 4,\n  },\n  {\n    lines: [\"01. Build\", \"Plant the grove\"],\n    matrix: GROUND,\n    size: \"text-sm\",\n    x: 2,\n    z: SECTION_GAP + 16,\n  },\n  {\n    lines: [\"02. Stack\", \"Raise the skyline\"],\n    matrix: GROUND,\n    size: \"text-sm\",\n    x: 2,\n    z: SECTION_GAP * 2 + 16,\n  },\n  {\n    lines: [\"03. Ship\", \"Cross the pond\"],\n    matrix: GROUND,\n    size: \"text-sm\",\n    x: 2,\n    z: SECTION_GAP * 3 + 16,\n  },\n  {\n    lines: [\"04. Start\", \"Join the world\"],\n    matrix: GROUND,\n    size: \"text-sm\",\n    x: 10,\n    z: WORLD_LEN + 4,\n  },\n];\n\ninterface VoxelLandingProps {\n  autoTour?: boolean;\n}\n\nconst clamp01 = (v: number) => Math.min(1, Math.max(0, v));\nconst easeInOut = (t: number) => (t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2);\n\n// Auto-tour keyframes: [time, scroll]. Holds at each section, long hold on START, rewind.\nconst TOUR: [number, number][] = [\n  [0, 0],\n  [2.8, 0],\n  [4.6, 0.25],\n  [5.6, 0.25],\n  [7.4, 0.5],\n  [8.4, 0.5],\n  [10.2, 0.75],\n  [11.2, 0.75],\n  [13, 1],\n  [17.5, 1],\n  [19.5, 0],\n  [21, 0],\n];\nconst [TOUR_LENGTH] = TOUR.at(-1) ?? [1];\n\nconst tourScroll = (t: number) => {\n  const cycle = t % TOUR_LENGTH;\n  let [t0, s0] = TOUR[0] ?? [0, 0];\n  for (const [t1, s1] of TOUR) {\n    if (cycle <= t1) {\n      const span = t1 - t0;\n      const p = span > 0 ? (cycle - t0) / span : 1;\n      return s0 + (s1 - s0) * easeInOut(p);\n    }\n    [t0, s0] = [t1, s1];\n  }\n  return 0;\n};\n\nconst VY = TILE * 1.05;\n\ninterface Drawable {\n  depth: number;\n  sx: number;\n  sy: number;\n  size: number;\n  h: number;\n  color: string;\n  alpha: number;\n}\n\ntype Push = (\n  x: number,\n  y: number,\n  z: number,\n  size: number,\n  h: number,\n  color: string,\n  alpha: number,\n) => void;\n\nconst pushWorld = (push: Push, world: Voxel[], now: number, atFinale: boolean) => {\n  const pulsePhase = Math.floor(now * 2.5);\n  const finaleRate = atFinale ? 0.34 : 0.16;\n  for (const v of world) {\n    let { color, alpha } = v;\n    if (v.kind === \"sign\") {\n      const rate = v.section === 4 ? finaleRate : 0.16;\n      if (hash(v.seed * 13.7 + pulsePhase * 3.3) < rate) {\n        color = accentAt(Math.floor(hash(v.seed + pulsePhase) * 4));\n      }\n    } else if (v.kind === \"road\") {\n      alpha = 0.35 + 0.65 * hash(v.seed * 7.1 + Math.floor(now * 3.2));\n    }\n    push(v.x, v.y, v.z, v.size, v.h, color, alpha);\n  }\n};\n\n// Legs alternate, tiny bob, everything snapped to half tiles.\nconst pushWalkers = (push: Push, walkers: Walker[], now: number, tick: number) => {\n  for (const w of walkers) {\n    const [wx, wz, walking] = walkerPos(w, now);\n    const step = walking && (tick + w.id) % 2 === 0;\n    const bob = step ? 0.12 : 0;\n    push(wx, 0, wz, 0.42, step ? 0.85 : 1, \"#23304a\", 1);\n    push(wx, 1 + bob, wz, 0.62, 1, w.body, 1);\n    push(wx, 2 + bob, wz, 0.5, 0.9, \"#dcc9a0\", 1);\n  }\n};\n\nconst pushSky = (push: Push, now: number, tick: number) => {\n  for (let i = 0; i < 5; i += 1) {\n    const bz = 20 + hash(i * 19.3) * (WORLD_LEN - 20);\n    const bx = ((now * 2.4 + hash(i * 7.7) * 40) % 44) - 22;\n    const flap = (tick + i) % 2 === 0 ? 0.5 : -0.5;\n    push(bx, 6, bz, 0.42, 0.5, \"#e8ecef\", 1);\n    push(bx + flap, 6.2, bz, 0.3, 0.35, \"#c9ced3\", 1);\n  }\n\n  // Clouds drift upwind (opposite the scroll direction).\n  const cloudSpan = WORLD_LEN + 60;\n  for (let i = 0; i < 7; i += 1) {\n    const cz =\n      ((((hash(i * 11.1) * cloudSpan - now * 0.5) % cloudSpan) + cloudSpan) % cloudSpan) - 20;\n    const cx = (hash(i * 5.3) - 0.5) * 26;\n    const qz = Math.round(cz * 4) / 4;\n    for (let p = 0; p < 4; p += 1) {\n      push(cx + (p > 1 ? 0.8 : 0), 12 + (p % 2) * 0.3, qz + p - 1.5, 0.8, 0.7, \"#e8ecef\", 0.92);\n    }\n  }\n\n  for (let i = 0; i < 2; i += 1) {\n    const bx = ((now * 1.1 + hash(i * 23.9) * 84) % 84) - 42;\n    const bz = 30 + hash(i * 9.7) * (WORLD_LEN - 40);\n    const qx = Math.round(bx * 4) / 4;\n    const lift = Math.round(Math.sin(now * 0.7 + i * 2.4) * 2) / 4;\n    push(qx, 10.4 + lift, bz, 0.5, 0.5, \"#7a5a3a\", 1);\n    push(qx, 11.4 + lift, bz, 1.1, 1.1, accentAt(i * 3), 1);\n  }\n};\n\nconst JETS = [\n  [0, 0],\n  [1.2, 0],\n  [-1.2, 0],\n  [0, 1.2],\n  [0, -1.2],\n] as const;\n\nconst pushTrucks = (push: Push, now: number) => {\n  const truckSpan = WORLD_LEN + 40;\n  for (let i = 0; i < 2; i += 1) {\n    const outbound = i === 0;\n    const p = (now * (2.8 + i * 0.6) + hash(i * 41.7) * truckSpan) % truckSpan;\n    const tz = outbound ? p - 20 : WORLD_LEN + 20 - p;\n    const qz = Math.round(tz * 4) / 4;\n    const tx = Math.round(roadX(qz) * 4) / 4;\n    const dir = outbound ? 1 : -1;\n    push(tx, 0, qz + 0.7 * dir, 0.5, 0.7, \"#e8ecef\", 1);\n    push(tx, 0, qz - 0.5 * dir, 0.62, 1, accentAt(i * 2 + 1), 1);\n  }\n};\n\nconst pushGround = (push: Push, now: number, tick: number) => {\n  // Fountain jets: chunky water columns rising and collapsing.\n  for (const [j, [jx, jz]] of JETS.entries()) {\n    const jetH = 1 + Math.floor((Math.sin(now * 2.2 + j * 1.7) + 1) * 1.4);\n    for (let y = 1; y <= jetH; y += 1) {\n      push(FOUNTAIN.x + jx, y * 0.6, FOUNTAIN.z + jz, 0.26, 0.5, \"#4a9de0\", 0.9);\n    }\n  }\n\n  pushTrucks(push, now);\n\n  const duckSpan = WORLD_LEN + 20;\n  const duckLead = (now * 0.8 + 5) % duckSpan;\n  for (let k = 0; k < 4; k += 1) {\n    const dz = duckLead - k * 1.4 - 10;\n    const qz = Math.round(dz * 4) / 4;\n    const dx = Math.round((roadX(qz) + 1.6) * 4) / 4;\n    const waddle = (tick + k) % 2 === 0 ? 0.06 : 0;\n    const s = k === 0 ? 1 : 0.7;\n    push(dx, waddle, qz, 0.34 * s, 0.45 * s, \"#e8ecef\", 1);\n    push(dx, 0.45 * s + waddle, qz + 0.22, 0.18 * s, 0.3 * s, \"#f0b429\", 1);\n  }\n\n  // Snail circling the pond — the slowest thing alive here.\n  const snailA = now * 0.12;\n  const sx = Math.round((12 + Math.cos(snailA) * 6.5) * 4) / 4;\n  const sz = Math.round((SECTION_GAP * 3 + Math.sin(snailA) * 9.5) * 4) / 4;\n  push(sx, 0, sz, 0.32, 0.3, \"#b98d4f\", 1);\n  push(sx, 0.3, sz - 0.2, 0.26, 0.4, \"#f0b429\", 1);\n};\n\nconst paint = (ctx: CanvasRenderingContext2D, drawables: Drawable[]) => {\n  for (const d of drawables) {\n    const w2 = TILE * d.size;\n    const h2 = HALF * d.size;\n    const v2 = VY * d.h;\n    ctx.globalAlpha = d.alpha;\n    ctx.fillStyle = d.color;\n    ctx.beginPath();\n    ctx.moveTo(d.sx, d.sy - h2);\n    ctx.lineTo(d.sx + w2, d.sy);\n    ctx.lineTo(d.sx, d.sy + h2);\n    ctx.lineTo(d.sx - w2, d.sy);\n    ctx.closePath();\n    ctx.fill();\n    ctx.fillStyle = shade(d.color, 0.62);\n    ctx.beginPath();\n    ctx.moveTo(d.sx - w2, d.sy);\n    ctx.lineTo(d.sx, d.sy + h2);\n    ctx.lineTo(d.sx, d.sy + h2 + v2);\n    ctx.lineTo(d.sx - w2, d.sy + v2);\n    ctx.closePath();\n    ctx.fill();\n    ctx.fillStyle = shade(d.color, 0.4);\n    ctx.beginPath();\n    ctx.moveTo(d.sx + w2, d.sy);\n    ctx.lineTo(d.sx, d.sy + h2);\n    ctx.lineTo(d.sx, d.sy + h2 + v2);\n    ctx.lineTo(d.sx + w2, d.sy + v2);\n    ctx.closePath();\n    ctx.fill();\n  }\n  ctx.globalAlpha = 1;\n};\n\nexport const VoxelLanding = ({ autoTour = true }: VoxelLandingProps) => {\n  const containerRef = useRef<HTMLDivElement | null>(null);\n  const canvasRef = useRef<HTMLCanvasElement | null>(null);\n  const htmlRef = useRef<HTMLDivElement | null>(null);\n  const [sectionIdx, setSectionIdx] = useState(0);\n  const [moved, setMoved] = useState(false);\n\n  useEffect(() => {\n    const container = containerRef.current;\n    const canvas = canvasRef.current;\n    const htmlLayer = htmlRef.current;\n    if (!container || !canvas || !htmlLayer) {\n      return;\n    }\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) {\n      return;\n    }\n\n    const world = buildWorld();\n    const walkers = buildWalkers();\n    const reducedMotion = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\n    let width = 0;\n    let height = 0;\n    let dotPattern: CanvasPattern | null = null;\n\n    const resize = () => {\n      const dpr = Math.min(window.devicePixelRatio, 2);\n      width = container.clientWidth;\n      height = container.clientHeight;\n      canvas.width = Math.round(width * dpr);\n      canvas.height = Math.round(height * dpr);\n      canvas.style.width = `${width}px`;\n      canvas.style.height = `${height}px`;\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n\n      const tile = document.createElement(\"canvas\");\n      tile.width = 18;\n      tile.height = 18;\n      const tileCtx = tile.getContext(\"2d\");\n      if (tileCtx) {\n        tileCtx.fillStyle = \"#0f1114\";\n        tileCtx.fillRect(0, 0, 18, 18);\n        tileCtx.fillStyle = \"#1a1d21\";\n        tileCtx.fillRect(8, 8, 2, 2);\n        dotPattern = ctx.createPattern(tile, \"repeat\");\n      }\n    };\n    resize();\n    const observer = new ResizeObserver(resize);\n    observer.observe(container);\n\n    let scroll = 0;\n    let target = 0;\n    let lastInput = -Infinity;\n    let tourOffset = 0;\n    let pausedAt: number | null = null;\n    let raf = 0;\n    let last = 0;\n    let shownSection = 0;\n    let shownMoved = false;\n    const start = performance.now();\n\n    const frame = (nowMs: number) => {\n      raf = requestAnimationFrame(frame);\n      if (nowMs - last < 1000 / 30) {\n        return;\n      }\n      last = nowMs;\n      const elapsed = (nowMs - start) / 1000;\n      const now = reducedMotion ? 0 : elapsed;\n\n      if (autoTour && !reducedMotion) {\n        if (elapsed - lastInput < 4) {\n          if (pausedAt === null) {\n            pausedAt = elapsed;\n          }\n        } else {\n          if (pausedAt !== null) {\n            tourOffset += elapsed - pausedAt;\n            pausedAt = null;\n          }\n          target = tourScroll(elapsed - tourOffset);\n        }\n      }\n\n      scroll += (target - scroll) * 0.09;\n      const camZ = scroll * WORLD_LEN;\n      // Keep world point (0, 0, camZ) at a fixed screen anchor: as camZ grows\n      // the world slides up-right, matching the original's scroll direction.\n      const offX = width / 2 + camZ * TILE;\n      const offY = height * 0.52 - camZ * HALF;\n\n      htmlLayer.style.transform = `translate3d(${offX}px, ${offY}px, 0)`;\n\n      ctx.fillStyle = dotPattern ?? \"#0f1114\";\n      ctx.fillRect(0, 0, width, height);\n\n      const nearest = Math.round(scroll * (SECTIONS.length - 1));\n      if (nearest !== shownSection) {\n        shownSection = nearest;\n        setSectionIdx(nearest);\n      }\n      if (!shownMoved && scroll > 0.02) {\n        shownMoved = true;\n        setMoved(true);\n      }\n\n      const atFinale = nearest === SECTIONS.length - 1 && Math.abs(scroll - 1) < 0.02;\n      const tick = Math.floor(now / 0.16);\n\n      const drawables: Drawable[] = [];\n      const push: Push = (x, y, z, size, h, color, alpha) => {\n        const sx = (x - z) * TILE + offX;\n        const sy = (x + z) * HALF + offY - y * VY;\n        if (sx < -70 || sx > width + 70 || sy < -90 || sy > height + 90) {\n          return;\n        }\n        drawables.push({ alpha, color, depth: (x + z) * 1000 + y, h, size, sx, sy });\n      };\n\n      pushWorld(push, world, now, atFinale);\n      pushWalkers(push, walkers, now, tick);\n      pushSky(push, now, tick);\n      pushGround(push, now, tick);\n\n      drawables.sort((a, b) => a.depth - b.depth);\n      paint(ctx, drawables);\n    };\n    raf = requestAnimationFrame(frame);\n\n    const nudge = (delta: number) => {\n      target = clamp01(target + delta);\n      lastInput = (performance.now() - start) / 1000;\n    };\n\n    const onWheel = (e: WheelEvent) => {\n      e.preventDefault();\n      nudge(e.deltaY * 0.0004);\n    };\n    container.addEventListener(\"wheel\", onWheel, { passive: false });\n\n    let dragY: number | null = null;\n    const onPointerDown = (e: PointerEvent) => {\n      dragY = e.clientY;\n      container.setPointerCapture(e.pointerId);\n    };\n    const onPointerMove = (e: PointerEvent) => {\n      if (dragY === null) {\n        return;\n      }\n      nudge((dragY - e.clientY) * 0.0016);\n      dragY = e.clientY;\n    };\n    const onPointerUp = () => {\n      dragY = null;\n    };\n    container.addEventListener(\"pointerdown\", onPointerDown);\n    container.addEventListener(\"pointermove\", onPointerMove);\n    container.addEventListener(\"pointerup\", onPointerUp);\n    container.addEventListener(\"pointercancel\", onPointerUp);\n\n    return () => {\n      cancelAnimationFrame(raf);\n      observer.disconnect();\n      container.removeEventListener(\"wheel\", onWheel);\n      container.removeEventListener(\"pointerdown\", onPointerDown);\n      container.removeEventListener(\"pointermove\", onPointerMove);\n      container.removeEventListener(\"pointerup\", onPointerUp);\n      container.removeEventListener(\"pointercancel\", onPointerUp);\n    };\n  }, [autoTour]);\n\n  const accent = SECTIONS[sectionIdx]?.accent ?? ACCENTS[0];\n\n  return (\n    <div\n      ref={containerRef}\n      className=\"relative h-full w-full touch-none select-none overflow-hidden bg-[#0f1114]\"\n    >\n      <canvas ref={canvasRef} className=\"absolute inset-0\" />\n\n      <div\n        ref={htmlRef}\n        className=\"pointer-events-none absolute left-0 top-0 will-change-transform\"\n      >\n        {LABELS.map((label) => (\n          <div\n            key={label.lines[0]}\n            className=\"absolute whitespace-nowrap\"\n            style={{\n              transform: `translate(${(label.x - label.z) * TILE}px, ${(label.x + label.z) * HALF}px) ${label.matrix}`,\n              transformOrigin: \"0 0\",\n            }}\n          >\n            <div className={`${label.size} font-bold tracking-[0.2em] text-neutral-200`}>\n              {label.lines[0]}\n            </div>\n            <div className=\"text-[10px] uppercase tracking-[0.3em] text-neutral-500\">\n              {label.lines[1]}\n            </div>\n          </div>\n        ))}\n      </div>\n\n      <div className=\"pointer-events-none absolute inset-0 font-mono uppercase\">\n        <div className=\"absolute left-6 top-5 flex items-center gap-2 text-[11px] font-bold tracking-[0.35em] text-neutral-200\">\n          <span className=\"inline-block size-2.5\" style={{ backgroundColor: accent }} />\n          Voxelworks\n        </div>\n        <div className=\"absolute right-6 top-5 text-[10px] tracking-[0.3em] text-neutral-500\">\n          STN 0{sectionIdx + 1} / 0{SECTIONS.length}\n        </div>\n\n        <div className=\"absolute bottom-5 left-1/2 flex -translate-x-1/2 gap-2\">\n          {SECTIONS.map((section, i) => (\n            <span\n              key={section.word}\n              className=\"size-2 border border-neutral-700 transition-colors duration-300\"\n              style={\n                i === sectionIdx ? { backgroundColor: accent, borderColor: accent } : undefined\n              }\n            />\n          ))}\n        </div>\n\n        <div className=\"absolute bottom-5 right-6 text-[10px] tracking-[0.3em] text-neutral-600\">\n          {SECTIONS[sectionIdx]?.word}\n        </div>\n\n        <div\n          className={`absolute bottom-16 left-6 text-[10px] tracking-[0.3em] text-neutral-500 transition-opacity duration-700 ${moved ? \"opacity-0\" : \"opacity-100 motion-safe:animate-pulse\"}`}\n        >\n          Scroll ▾\n        </div>\n      </div>\n    </div>\n  );\n};\n","path":"/voxel-landing.tsx","target":"uicapsule/voxel-landing/voxel-landing.tsx","type":"registry:file"}],"homepage":"https://www.uicapsule.com/ui/voxel-landing","name":"voxel-landing","registryDependencies":[],"title":"Voxel Landing","type":"registry:block"}