# Ray-Traced Light Box History Timeline with a Sliding Film Negative Strip and a Print That Develops Beside It

> Free React history timeline: pick a year and a strip of colour negatives slides across a glowing light box while its print develops on the desk.

A shadcn/ui block from the [@weinc](https://we.inc/components?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/weinc/ray-traced-light-box-history-timeline-with-a-sliding-film-negative-strip-and-a-print-that-develops-beside-it).

- Page, with a live preview: https://shadcn.exchange/r/weinc/ray-traced-light-box-history-timeline-with-a-sliding-film-negative-strip-and-a-print-that-develops-beside-it
- Screenshot: https://shadcn.exchange/shot/weinc/ray-traced-light-box-history-timeline-with-a-sliding-film-negative-strip-and-a-print-that-develops-beside-it/light.png
- Registry item: https://we.inc/r/ray-traced-light-box-history-timeline-with-a-sliding-film-negative-strip-and-a-print-that-develops-beside-it.json
- Listed: 2026-10-28, source last changed 2026-10-28

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/weinc/ray-traced-light-box-history-timeline-with-a-sliding-film-negative-strip-and-a-print-that-develops-beside-it.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Files

### `components/we-inc/ray-traced-light-box-history-timeline-with-a-sliding-film-negative-strip-and-a-print-that-develops-beside-it.tsx`

```tsx
import { useEffect, useRef, useState } from "react";

// The years. Each one is a frame on the strip of negatives, and the print on the desk beside the box.
const years = [
  { year: "1968", title: "A wheel in the coal shed", body: "Ada Marrow buys a kick wheel at a school sale and throws her first mugs in the shed behind the house, firing them in a neighbour's kiln.", photo: "https://images.pexels.com/photos/2556276/pexels-photo-2556276.jpeg?auto=compress&cs=tinysrgb&w=640", tone: "#8a7a6c" },
  { year: "1974", title: "Slabs, tools and a first order", body: "A café on the high street asks for sixty plates. She learns slab work to make them all the same size, and the kitchen table becomes a bench.", photo: "https://images.pexels.com/photos/5642023/pexels-photo-5642023.jpeg?auto=compress&cs=tinysrgb&w=640", tone: "#a39a90" },
  { year: "1981", title: "The big kiln is lit", body: "The pottery moves into a brick workshop on Tenter Street and builds its own kiln. Whole shelves of plates go in at once and come out warm a day later.", photo: "https://images.pexels.com/photos/9736509/pexels-photo-9736509.jpeg?auto=compress&cs=tinysrgb&w=640", tone: "#8c6a4f" },
  { year: "1993", title: "Bowls by the stack", body: "Four throwers now share the wheel room. The plain brown bowl that started as a seconds line becomes the piece people ask for by name.", photo: "https://images.pexels.com/photos/33703946/pexels-photo-33703946.jpeg?auto=compress&cs=tinysrgb&w=640", tone: "#6b4a34" },
  { year: "2006", title: "A shop with the door open", body: "The front room is cleared and shelved. Visitors can buy straight from the racks and watch the kiln being loaded through the back window.", photo: "https://images.pexels.com/photos/6611184/pexels-photo-6611184.jpeg?auto=compress&cs=tinysrgb&w=640", tone: "#9a9894" },
  { year: "2019", title: "Handed to the next pair", body: "Ada's apprentice of twelve years takes over the kiln book. The glazes, the stamp on the foot and the Friday firing all stay the same.", photo: "https://images.pexels.com/photos/6023601/pexels-photo-6023601.jpeg?auto=compress&cs=tinysrgb&w=640", tone: "#b49a80" },
];
const START = 2; // the year it opens on
const COMPANY = "Marrow Lane Pottery";
const STRAP = "Our story, one frame at a time";
const FONT = '"Inter Tight", "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif';
const SERIF = '"Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua", Georgia, serif';
const N = years.length;

// Scene units. Frame k of the strip sits at x = (k - strip) * PITCH, so the chosen frame comes to rest at x = 0.
const PITCH = 1.1, TOP = 0.42;

const VERT = `ATTR vec2 aPos; void main(){ gl_Position = vec4(aPos, 0.0, 1.0); }`;
const TONE = `
vec3 tone(vec3 x){ x *= 0.85; vec3 a = x * (2.51 * x + 0.03); vec3 b = x * (2.43 * x + 0.59) + 0.14; return pow(clamp(a / b, 0.0, 1.0), vec3(1.0 / 2.2)); }
`;

// A light box in a dim room: opal glass lit from inside, a strip of colour negatives across it,
// a desk lamp over a print lying on a dark walnut desk.
const SCENE = `
precision highp float;
uniform vec2 uRes; uniform vec4 uJit; uniform float uSeed;
uniform vec3 uCam; uniform vec3 uTgt; uniform float uFoc; uniform vec2 uShift; uniform float uFocus;
uniform float uSel; uniform float uHot; uniform float uCur; uniform float uDev;
uniform vec2 uBox; uniform vec4 uPrint; uniform float uRot; uniform vec3 uLamp;
uniform sampler2D uTex; uniform float uDirect;
const float H = ${TOP.toFixed(2)}; const float FY = ${(TOP + 0.008).toFixed(3)}; const float SW = 0.52; const float P = ${PITCH.toFixed(2)};
const float IX = 0.5; const float IZ = 0.335; const float NF = ${N.toFixed(1)};
const vec3 GLOW = vec3(1.0, 0.95, 0.86);
const vec3 LAMP = vec3(1.0, 0.74, 0.46);
const vec3 BASE = vec3(0.98, 0.6, 0.3);
${TONE}
float h21(vec2 p){ vec3 q = fract(vec3(p.xyx) * 0.1031); q += dot(q, q.yzx + 33.33); return fract((q.x + q.y) * q.z); }
float n2(vec2 x){ vec2 i = floor(x); vec2 f = fract(x); f = f * f * (3.0 - 2.0 * f);
  return mix(mix(h21(i), h21(i + vec2(1.0, 0.0)), f.x), mix(h21(i + vec2(0.0, 1.0)), h21(i + vec2(1.0, 1.0)), f.x), f.y); }
float sd2Box(vec2 p, vec2 b, float r){ vec2 q = abs(p) - b + r; return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r; }
vec2 boxSpan(vec3 ro, vec3 rd, vec3 lo, vec3 hi){
  vec3 inv = 1.0 / (rd + vec3(1e-6)); vec3 a = (lo - ro) * inv; vec3 b = (hi - ro) * inv;
  vec3 mn = min(a, b); vec3 mx = max(a, b);
  return vec2(max(max(mn.x, mn.y), max(mn.z, 0.0)), min(mx.x, min(mx.y, mx.z)));
}
float rnd(float k){ return h21(gl_FragCoord.xy * 0.913 + vec2(uSeed * 97.1 + k, k * 13.7)); }

// ---- the photographs, from the atlas: 3 across, 2 down ----
vec3 photoS(float k, vec2 uv){
  uv = clamp(uv, 0.003, 0.997);
  vec2 cell = vec2(mod(k, 3.0), floor(k / 3.0));
  return texture2D(uTex, (cell + vec2(uv.x, 1.0 - uv.y)) / vec2(3.0, 2.0)).rgb;
}
vec3 photo(float k, vec2 uv){ return pow(photoS(k, uv), vec3(2.2)); }

// ---- the opal glass: an even glow, brighter where the lamp inside sits under the chosen frame ----
float glassEmit(vec2 xz){
  float edge = min(uBox.x - abs(xz.x), uBox.y - abs(xz.y));
  float e = 0.9 * (0.62 + 0.38 * smoothstep(0.0, 1.2, edge)) * (1.0 - 0.18 * smoothstep(0.0, 1.0, abs(xz.x) / uBox.x));
  float r = sd2Box(xz, vec2(IX + 0.1, SW + 0.02), 0.1);
  e += uHot * 0.75 * smoothstep(0.05, -0.03, r) + uHot * 0.25 * smoothstep(0.5, 0.0, r);
  e *= 0.97 + 0.06 * n2(xz * 23.0);
  return e;
}

// ---- the film: orange base, clear sprocket holes, the image as a colour negative ----
vec3 filmTrans(vec2 xz){
  float u = xz.x / P + uSel;
  float k = floor(u + 0.5);
  float fx = (u - k) * P;
  float az = abs(xz.y);
  vec3 tr = BASE * 0.62;
  if (az > 0.39 && az < 0.48) {
    float hx = mod(xz.x + uSel * P, P / 8.0) - P / 16.0;
    float hole = sd2Box(vec2(hx, az - 0.435), vec2(0.026, 0.02), 0.008);
    tr = mix(vec3(1.0), tr, smoothstep(-0.002, 0.003, hole));
  }
  if (abs(fx) < IX && az < IZ) {
    vec2 uv = vec2(fx / (2.0 * IX) + 0.5, xz.y / (2.0 * IZ) + 0.5);
    float inside = smoothstep(0.0, 0.006, IX - abs(fx)) * smoothstep(0.0, 0.006, IZ - az);
    vec3 neg;
    if (k >= 0.0 && k < NF) {
      vec3 ph = photoS(k, uv);
      neg = BASE * (vec3(0.02) + 0.95 * pow(clamp(1.0 - ph * 1.08, 0.0, 1.0), vec3(2.3)));
    } else {
      neg = BASE * 0.06; // the fogged leader and tail
    }
    tr = mix(tr, neg, inside);
  }
  // a little edge printing along the far side: dashes and a frame mark
  if (xz.y < -0.355 && xz.y > -0.385) {
    float d = mod(xz.x + uSel * P + 0.2, P);
    float mark = step(0.08, d) * step(d, 0.2) + step(0.5, d) * step(d, 0.53) + step(0.58, d) * step(d, 0.61);
    tr *= 1.0 - 0.75 * mark;
  }
  tr *= 0.35 + 0.65 * smoothstep(0.0, 0.012, SW - az);
  return tr;
}
bool onStrip(vec2 xz){
  float a = (-1.4 - uSel) * P, b = (NF + 0.4 - uSel) * P;
  return abs(xz.y) < SW && xz.x > a && xz.x < b;
}

// What glossy things see: a dark room, a pale ceiling bounce, and the lamp's warm bulb.
vec3 env(vec3 r){
  vec3 c = mix(vec3(0.010, 0.009, 0.008), vec3(0.03, 0.028, 0.026), smoothstep(-0.1, 0.8, r.y));
  vec3 d = normalize(uLamp - vec3(0.0, 0.4, 1.0));
  c += LAMP * 22.0 * smoothstep(0.986, 0.996, dot(r, d));
  return c;
}

// Light at a point on the desk or print: the lamp (with a shadow from the box) and the box's spill.
vec3 deskLight(vec3 p, vec3 n){
  vec3 lp = uLamp + vec3(rnd(1.0) - 0.5, 0.0, rnd(2.0) - 0.5) * 0.7;
  vec3 L = lp - p; float d = length(L); L /= d;
  float spot = smoothstep(0.8, 0.95, dot(-L, normalize(vec3(uPrint.x, 0.0, uPrint.y) - uLamp)));
  float sh = 1.0;
  vec2 tb = boxSpan(p + n * 0.002, L, vec3(-uBox.x, 0.0, -uBox.y), vec3(uBox.x, H, uBox.y));
  if (tb.x < tb.y && tb.x < d) sh = 0.0;
  vec3 c = LAMP * 30.0 * max(dot(n, L), 0.0) / (d * d) * (0.04 + 0.96 * spot) * sh;
  float fd = sd2Box(p.xz, uBox, 0.04);
  c += GLOW * 0.85 * exp(-max(fd, 0.0) * 2.6) * (0.35 + 0.65 * smoothstep(0.0, 0.2, fd));
  c += vec3(0.012, 0.011, 0.01);
  return c;
}

// The box seen in a reflection: its glowing glass edge and its dark painted body.
vec3 boxSeen(vec3 ro, vec3 rd){
  vec2 tb = boxSpan(ro, rd, vec3(-uBox.x, 0.0, -uBox.y), vec3(uBox.x, H, uBox.y));
  if (tb.x < tb.y && tb.x > 0.001) {
    vec3 p = ro + rd * tb.x;
    if (p.y > H - 0.055) return GLOW * 0.9;
    return vec3(0.02, 0.019, 0.018);
  }
  return env(rd);
}

vec3 shadeDesk(vec3 p, vec3 rd){
  float w = n2(vec2(p.x * 0.35, p.z * 2.2)) * 1.6 + n2(vec2(p.x * 1.4, p.z * 6.0)) * 0.3;
  float ring = sin(p.z * 34.0 + w * 6.0);
  float fine = n2(vec2(p.x * 2.0, p.z * 160.0));
  vec3 a = vec3(0.075, 0.045, 0.028) * (0.82 + 0.12 * ring + 0.14 * fine);
  float fd = sd2Box(p.xz, uBox, 0.04);
  float occ = 0.3 + 0.7 * smoothstep(0.0, 0.35, fd);
  vec2 q = p.xz - uPrint.xy; float cr = cos(uRot), sr = sin(uRot); q = vec2(cr * q.x - sr * q.y, sr * q.x + cr * q.y);
  occ *= 0.45 + 0.55 * smoothstep(-0.01, 0.07, sd2Box(q, uPrint.zw, 0.01));
  // the end of the strip that hangs past the case throws a soft shadow just beside it
  vec2 sp = p.xz + vec2(0.0, 0.12);
  if (onStrip(vec2(sp.x, 0.0)) && fd > 0.0) occ *= mix(1.0, 0.35, smoothstep(SW + 0.08, SW - 0.12, abs(sp.y)));
  vec3 n = vec3(0.0, 1.0, 0.0);
  vec3 col = a * deskLight(p, n) * occ;
  // satin varnish: a blurred reflection of the box edge and the lamp
  vec3 nn = normalize(n + vec3(rnd(3.0) - 0.5, 0.0, rnd(4.0) - 0.5) * 0.16);
  vec3 r = reflect(rd, nn);
  float F = 0.03 + 0.97 * pow(1.0 - clamp(dot(-rd, nn), 0.0, 1.0), 5.0);
  col += F * boxSeen(p + n * 0.002, r) * 0.55 * occ;
  return col;
}

vec3 shadePrint(vec3 p, vec3 rd, vec2 q){
  vec2 inner = uPrint.zw - vec2(0.085);
  vec3 paper = vec3(0.86, 0.84, 0.8);
  vec3 a = paper;
  if (abs(q.x) < inner.x && abs(q.y) < inner.y) {
    vec2 uv = vec2(q.x / (2.0 * inner.x) + 0.5, q.y / (2.0 * inner.y) + 0.5);
    vec3 ph = photo(uCur, uv) * 0.92;
    float l = dot(ph, vec3(0.3, 0.55, 0.15));
    // shadows come up first, then the mid tones, then the colour settles
    float dev = smoothstep(0.0, 1.0, uDev * 1.7 - pow(l, 0.5) * 0.7);
    vec3 warm = mix(paper, ph * vec3(1.08, 0.96, 0.84), dev);
    a = mix(warm, ph, smoothstep(0.55, 1.0, uDev));
  }
  vec3 n = vec3(0.0, 1.0, 0.0);
  vec3 col = a * deskLight(p, n);
  vec3 r = reflect(rd, normalize(n + vec3(rnd(5.0) - 0.5, 0.0, rnd(6.0) - 0.5) * 0.2));
  float F = 0.035 + 0.965 * pow(1.0 - clamp(dot(-rd, n), 0.0, 1.0), 5.0);
  col += F * boxSeen(p + n * 0.002, r) * 0.6;
  return col;
}

vec3 shadeFilm(vec3 p, vec3 rd, bool over){
  vec3 tr = filmTrans(p.xz);
  vec3 col;
  if (over) col = GLOW * glassEmit(p.xz) * tr;
  else col = tr * 0.25 * deskLight(p, vec3(0.0, 1.0, 0.0)) * vec3(0.5, 0.35, 0.25) + BASE * 0.006;
  vec3 n = normalize(vec3(0.0, 1.0, 0.0) + vec3(n2(p.xz * 3.0) - 0.5, 0.0, n2(p.zx * 3.0 + 7.0) - 0.5) * 0.03);
  vec3 r = reflect(rd, n);
  float F = 0.04 + 0.96 * pow(1.0 - clamp(dot(-rd, n), 0.0, 1.0), 5.0);
  col += F * env(r) * 1.2;
  return col;
}

vec3 shadeBox(vec3 p, vec3 rd){
  vec3 q = p / vec3(uBox.x, H, uBox.y);
  vec3 a = abs(q);
  vec3 n = a.x > a.z ? vec3(sign(q.x), 0.0, 0.0) : vec3(0.0, 0.0, sign(q.z));
  if (a.y > max(a.x, a.z) && q.y > 0.0) n = vec3(0.0, 1.0, 0.0);
  if (n.y > 0.5) {
    // a satin aluminium bezel holds the glass
    float rim = min(uBox.x - abs(p.x), uBox.y - abs(p.z));
    if (rim < 0.11) {
      vec3 r = reflect(rd, n);
      float lip = smoothstep(0.11, 0.095, rim) * smoothstep(0.0, 0.02, rim);
      vec3 al = vec3(0.09, 0.088, 0.085) * deskLight(p, n) * 2.0 + env(r) * 0.35;
      al += GLOW * 0.5 * exp(-(0.11 - rim) * 30.0) * 0.4;
      return mix(al, al * 1.8, 1.0 - lip) * (0.6 + 0.4 * smoothstep(0.0, 0.015, rim));
    }
    // the bare glass round the strip
    vec3 col = GLOW * glassEmit(p.xz);
    vec3 r = reflect(rd, n);
    col += (0.04 + 0.96 * pow(1.0 - clamp(dot(-rd, n), 0.0, 1.0), 5.0)) * env(r);
    return col;
  }
  // the edge of the opal glass shows as a glowing band along the top of the case
  float band = smoothstep(H - 0.058, H - 0.05, p.y);
  float lip = smoothstep(H - 0.075, H - 0.06, p.y) * (1.0 - band);
  vec3 body = vec3(0.03, 0.029, 0.028);
  vec3 col = body * deskLight(p, n) * 1.4;
  vec3 r = reflect(rd, n);
  col += (0.05 + 0.95 * pow(1.0 - clamp(dot(-rd, n), 0.0, 1.0), 5.0)) * env(r) * 0.8;
  col += vec3(0.5, 0.48, 0.45) * lip * 0.08;
  col = mix(col, GLOW * vec3(0.92, 1.0, 0.94) * 0.9, band);
  // the case sits in its own glow at the foot
  col *= 0.55 + 0.45 * smoothstep(0.0, 0.12, p.y);
  return col;
}

void main(){
  vec2 fc = gl_FragCoord.xy + uJit.xy;
  vec2 uv = (fc - 0.5 * uRes) / uRes.y - uShift;
  vec3 fw = normalize(uTgt - uCam); vec3 rt = normalize(cross(fw, vec3(0.0, 1.0, 0.0))); vec3 up = cross(rt, fw);
  vec3 rd = normalize(fw * uFoc + rt * uv.x + up * uv.y);
  vec3 ro = uCam;
  vec3 fp = ro + rd * (uFocus / dot(rd, fw));
  ro += rt * uJit.z + up * uJit.w; rd = normalize(fp - ro);

  float best = 1e9; int what = 0; vec2 pq = vec2(0.0);
  if (rd.y < 0.0) {
    float tf = (FY - ro.y) / rd.y; vec3 p = ro + rd * tf;
    if (onStrip(p.xz)) { best = tf; what = 1; }
    float tp = (0.006 - ro.y) / rd.y; p = ro + rd * tp;
    vec2 q = p.xz - uPrint.xy; float cr = cos(uRot), sr = sin(uRot); q = vec2(cr * q.x - sr * q.y, sr * q.x + cr * q.y);
    if (tp < best && abs(q.x) < uPrint.z && abs(q.y) < uPrint.w) { best = tp; what = 2; pq = q; }
  }
  vec2 tb = boxSpan(ro, rd, vec3(-uBox.x, 0.0, -uBox.y), vec3(uBox.x, H, uBox.y));
  if (tb.x < tb.y && tb.x < best) { best = tb.x; what = 3; }
  if (what == 0 && rd.y < 0.0) { best = -ro.y / rd.y; what = 4; }

  vec3 col = vec3(0.004);
  vec3 p = ro + rd * best;
  if (what == 1) col = shadeFilm(p, rd, abs(p.x) < uBox.x && abs(p.z) < uBox.y);
  else if (what == 2) col = shadePrint(p, rd, pq);
  else if (what == 3) col = shadeBox(p, rd);
  else if (what == 4) col = shadeDesk(p, rd) * exp(-max(best - 18.0, 0.0) * 0.15);
  col = min(col, vec3(30.0));
  if (uDirect > 0.5) col = tone(col);
  outC = vec4(col, 1.0);
}
`;

// Developing the plate: bloom off the glass, the filmic curve, a vignette and grain.
const POST = `
precision highp float;
uniform sampler2D uAcc; uniform vec2 uRes; uniform float uGrain; uniform vec2 uCentre;
${TONE}
float h21(vec2 p){ vec3 q = fract(vec3(p.xyx) * 0.1031); q += dot(q, q.yzx + 33.33); return fract((q.x + q.y) * q.z); }
void main(){
  vec2 uv = gl_FragCoord.xy / uRes;
  vec3 c = texture2D(uAcc, uv).rgb;
  vec3 b = vec3(0.0);
  for (int i = 0; i < 24; i++) {
    float a = float(i) * 2.39996; float r = sqrt((float(i) + 0.5) / 24.0);
    vec3 s = texture2D(uAcc, uv + vec2(cos(a), sin(a)) * r * 0.035 * vec2(uRes.y / uRes.x, 1.0)).rgb;
    b += min(max(s - 1.6, 0.0), vec3(8.0)) * (1.0 - 0.7 * r);
  }
  c += b * 0.012 * vec3(1.0, 0.9, 0.76);
  vec2 q = (uv - uCentre) * vec2(uRes.x / uRes.y, 1.0);
  c *= 1.0 - 0.35 * smoothstep(0.3, 1.2, length(q));
  c = tone(c);
  c += (h21(gl_FragCoord.xy + uGrain * 91.7) - 0.5) * 0.022;
  outC = vec4(c, 1.0);
}
`;

// No WebGL: the same desk built from CSS, with the photographs themselves.
function Still({ act }: { act: number }) {
  return (
    <div aria-hidden="true" className="absolute inset-x-4 top-[14%] sm:inset-x-10">
      <div className="relative mx-auto h-[170px] max-w-[900px] overflow-hidden rounded-[6px]" style={{ background: "#f3ede2", boxShadow: "0 0 80px 10px rgba(255,236,205,0.18), inset 0 0 40px rgba(220,200,170,0.6)" }}>
        <div className="absolute left-1/2 top-[30px] flex h-[110px] items-center gap-[6px] px-2 transition-transform duration-700 ease-[cubic-bezier(0.3,1.3,0.5,1)] motion-reduce:transition-none" style={{ background: "rgba(214,128,62,0.82)", transform: `translateX(${-(78 + act * 146)}px)`, width: N * 146 + 10 }}>
          {years.map((y, i) => (
            <div key={y.year} className="h-[86px] w-[140px] shrink-0 bg-cover bg-center" style={{ backgroundImage: `url(${y.photo})`, backgroundColor: y.tone, filter: "invert(1) sepia(0.6) saturate(2.2) hue-rotate(-12deg)", opacity: i === act ? 1 : 0.8 }} />
          ))}
        </div>
      </div>
    </div>
  );
}

export default function LightBoxHistoryTimeline() {
  const stage = useRef<HTMLDivElement>(null);
  const canvas = useRef<HTMLCanvasElement>(null);
  const tags = useRef<(HTMLButtonElement | null)[]>([]);
  const [act, setAct] = useState(START);
  const [shown, setShown] = useState<"wait" | "gl" | "still">("wait");
  const actRef = useRef(START);
  const api = useRef<{ go: (i: number, fromUser: boolean) => void } | null>(null);

  useEffect(() => {
    const box = stage.current, cv = canvas.current;
    if (!box || !cv) return;
    const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    const opts = { antialias: false, alpha: false, powerPreference: "high-performance" as const };
    const gl2 = cv.getContext("webgl2", opts) as WebGL2RenderingContext | null;
    const gl = (gl2 ?? cv.getContext("webgl", opts)) as WebGLRenderingContext | null;
    if (!gl) { setShown("still"); return; }
    const canAcc = !!gl2 && (!!gl2.getExtension("EXT_color_buffer_float") || !!gl2.getExtension("EXT_color_buffer_half_float"));
    const info = gl.getExtension("WEBGL_debug_renderer_info");
    const renderer = info ? String(gl.getParameter(info.UNMASKED_RENDERER_WEBGL)) : "";
    const software = /swiftshader|llvmpipe|software|basic render/i.test(renderer);
    const still = reduce || software;

    const head = gl2 ? "#version 300 es\n#define ATTR in\n" : "#define ATTR attribute\n";
    const fhead = gl2 ? "#version 300 es\n#define texture2D texture\nprecision highp float;\nout vec4 outC;\n" : "#define outC gl_FragColor\n";
    const compile = (type: number, src: string) => {
      const sh = gl.createShader(type);
      if (!sh) return null;
      gl.shaderSource(sh, src); gl.compileShader(sh);
      return gl.getShaderParameter(sh, gl.COMPILE_STATUS) ? sh : null;
    };
    const link = (frag: string) => {
      const v = compile(gl.VERTEX_SHADER, head + VERT), f = compile(gl.FRAGMENT_SHADER, fhead + frag);
      const p = gl.createProgram();
      if (!v || !f || !p) return null;
      gl.attachShader(p, v); gl.attachShader(p, f); gl.bindAttribLocation(p, 0, "aPos"); gl.linkProgram(p);
      return gl.getProgramParameter(p, gl.LINK_STATUS) ? p : null;
    };
    const scene = link(SCENE), post = canAcc ? link(POST) : null;
    if (!scene || (canAcc && !post)) { setShown("still"); return; }
    const U = (p: WebGLProgram, n: string) => gl.getUniformLocation(p, n);
    const su = Object.fromEntries(["uRes", "uJit", "uSeed", "uCam", "uTgt", "uFoc", "uShift", "uFocus", "uSel", "uHot", "uCur", "uDev", "uBox", "uPrint", "uRot", "uLamp", "uTex", "uDirect"].map((n) => [n, U(scene, n)]));
    const pu = post ? Object.fromEntries(["uAcc", "uRes", "uGrain", "uCentre"].map((n) => [n, U(post, n)])) : {};

    const quad = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, quad);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
    gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);

    // The photographs go into one atlas, 3 across and 2 down, each cropped to the 3:2 frame.
    const CW = 480, CH = 320;
    const atlas = document.createElement("canvas");
    atlas.width = CW * 3; atlas.height = CH * 2;
    const ag = atlas.getContext("2d");
    const tex = gl.createTexture();
    const upload = () => {
      gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, tex);
      gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, atlas);
      gl.generateMipmap(gl.TEXTURE_2D);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
    };
    if (ag) years.forEach((y, i) => { ag.fillStyle = y.tone; ag.fillRect((i % 3) * CW, Math.floor(i / 3) * CH, CW, CH); });
    upload();
    let alive = true;
    years.forEach((y, i) => {
      const img = new Image();
      img.crossOrigin = "anonymous";
      img.onload = () => {
        if (!alive || !ag) return;
        const k = Math.max(CW / img.naturalWidth, CH / img.naturalHeight);
        const w = img.naturalWidth * k, h = img.naturalHeight * k;
        ag.drawImage(img, (i % 3) * CW + (CW - w) / 2, Math.floor(i / 3) * CH + (CH - h) / 2, w, h);
        upload(); s.count = 0; s.fresh = true; wake();
      };
      img.src = y.photo;
    });

    const acc = canAcc ? gl.createTexture() : null;
    const fbo = canAcc ? gl.createFramebuffer() : null;
    let W = 0, H = 0, scale = 1, cssW = 1, cssH = 1;
    const s = {
      sel: START, v: 0, target: START, hot: 1, cur: START, dev: 1, want: START,
      px: 0, py: 0, sx: 0, sy: 0, count: 0, fresh: true, frame: 0, slow: 0,
      visible: true, raf: 0, last: 0, t0: 0, touched: false, demoAt: 0, first: true,
    };
    const size = () => {
      const r = box.getBoundingClientRect();
      cssW = Math.max(1, r.width); cssH = Math.max(1, r.height);
      const dpr = Math.min(software ? 1 : 1.5, window.devicePixelRatio || 1) * scale;
      const k = Math.min(1, Math.sqrt(1.4e6 / (cssW * cssH * dpr * dpr)));
      const w = Math.max(2, Math.round(cssW * dpr * k)), h = Math.max(2, Math.round(cssH * dpr * k));
      if (w === W && h === H) return;
      W = w; H = h; cv.width = W; cv.height = H;
      if (gl2 && acc && fbo) {
        gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, acc);
        gl2.texImage2D(gl.TEXTURE_2D, 0, gl2.RGBA16F, W, H, 0, gl.RGBA, gl2.HALF_FLOAT, null);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
        gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
        gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, acc, 0);
        gl.bindFramebuffer(gl.FRAMEBUFFER, null);
      }
      s.count = 0; s.fresh = true;
    };
    const halton = (i: number, b: number) => { let f = 1, r = 0; while (i > 0) { f /= b; r += f * (i % b); i = Math.floor(i / b); } return r; };

    // Wide screens: a long box across the top, the print lying in front of it to the left.
    // Narrow screens: a shorter box seen from higher up, the print below it.
    const layout = () => {
      const wide = cssW >= 900, aspect = cssW / cssH;
      if (wide) return {
        wide, box: [4.6, 1.4], print: [-2.55, 3.55, 1.42, 0.98], rot: 0.075, lamp: [-3.6, 5.2, 5.6],
        tgt: [0.15, 0.0, 1.75], el: 0.74, az: 0.04, dist: 13.2, foc: 1.95 * Math.min(1, aspect / 1.75), shift: [0.0, -0.02],
      };
      return {
        wide, box: [2.25, 1.15], print: [0.05, 3.25, 1.32, 0.9], rot: -0.06, lamp: [-1.6, 5.4, 5.2],
        tgt: [0.0, 0.0, 1.9], el: 1.0, az: 0.0, dist: 11.0, foc: 1.12 * Math.min(1.2, aspect / 0.5), shift: [0.0, 0.15],
      };
    };
    const camera = () => {
      const l = layout();
      const el = l.el - s.sy * 0.02, az = l.az + s.sx * 0.05;
      const tgt = l.tgt;
      const cam = [tgt[0] + l.dist * Math.sin(az) * Math.cos(el), tgt[1] + l.dist * Math.sin(el), tgt[2] + l.dist * Math.cos(az) * Math.cos(el)];
      const fw = [tgt[0] - cam[0], tgt[1] - cam[1], tgt[2] - cam[2]];
      const fl = Math.hypot(fw[0], fw[1], fw[2]); fw[0] /= fl; fw[1] /= fl; fw[2] /= fl;
      const rt = [-fw[2], 0, fw[0]]; const rl = Math.hypot(rt[0], rt[2]); rt[0] /= rl; rt[2] /= rl;
      const up = [rt[1] * fw[2] - rt[2] * fw[1], rt[2] * fw[0] - rt[0] * fw[2], rt[0] * fw[1] - rt[1] * fw[0]];
      return { ...l, cam, fw, rt, up };
    };
    type Cam = ReturnType<typeof camera>;
    const project = (c: Cam, p: number[]) => {
      const d = [p[0] - c.cam[0], p[1] - c.cam[1], p[2] - c.cam[2]];
      const z = d[0] * c.fw[0] + d[1] * c.fw[1] + d[2] * c.fw[2];
      const x = (c.foc * (d[0] * c.rt[0] + d[1] * c.rt[1] + d[2] * c.rt[2])) / z + c.shift[0];
      const y = (c.foc * (d[0] * c.up[0] + d[1] * c.up[1] + d[2] * c.up[2])) / z + c.shift[1];
      return [cssW / 2 + x * cssH, cssH / 2 - y * cssH];
    };
    // The year buttons ride on the desk under their frames and slide with the strip.
    const placeTags = (c: Cam) => {
      for (let i = 0; i < N; i++) {
        const el = tags.current[i];
        if (!el) continue;
        const x = (i - s.sel) * PITCH;
        const [sx, sy] = project(c, [x, 0, c.box[1] + 0.28]);
        const off = Math.abs(x) > c.box[0] + 0.3;
        el.style.transform = `translate(${sx.toFixed(1)}px, ${sy.toFixed(1)}px) translate(-50%, 0)`;
        el.style.opacity = off ? "0.35" : "1";
      }
    };

    const draw = (alpha: number, sample: number) => {
      const c = camera();
      placeTags(c);
      const jx = sample ? halton(sample, 2) - 0.5 : 0, jy = sample ? halton(sample, 3) - 0.5 : 0;
      const la = halton(sample, 5) * Math.PI * 2, lr = sample ? Math.sqrt(halton(sample, 7)) * 0.05 : 0;
      gl.useProgram(scene);
      gl.bindFramebuffer(gl.FRAMEBUFFER, canAcc ? fbo : null);
      gl.viewport(0, 0, W, H);
      if (canAcc && alpha < 1) { gl.enable(gl.BLEND); gl.blendColor(0, 0, 0, alpha); gl.blendFunc(gl.CONSTANT_ALPHA, gl.ONE_MINUS_CONSTANT_ALPHA); } else gl.disable(gl.BLEND);
      gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, tex);
      gl.uniform2f(su.uRes, W, H);
      gl.uniform4f(su.uJit, canAcc ? jx : 0, canAcc ? jy : 0, canAcc ? Math.cos(la) * lr : 0, canAcc ? Math.sin(la) * lr : 0);
      gl.uniform1f(su.uSeed, canAcc && sample ? (sample * 0.61803) % 1 || 0.5 : 0.37);
      gl.uniform3f(su.uCam, c.cam[0], c.cam[1], c.cam[2]); gl.uniform3f(su.uTgt, c.tgt[0], c.tgt[1], c.tgt[2]);
      gl.uniform1f(su.uFoc, c.foc);
      gl.uniform2f(su.uShift, c.shift[0], c.shift[1]);
      // focus halfway between the lit frame and the print, so both stay crisp and the far desk falls away
      const fx = (c.print[0]) * 0.4, fz = c.print[1] * 0.45;
      gl.uniform1f(su.uFocus, (fx - c.cam[0]) * c.fw[0] + (0.2 - c.cam[1]) * c.fw[1] + (fz - c.cam[2]) * c.fw[2]);
      gl.uniform1f(su.uSel, s.sel); gl.uniform1f(su.uHot, s.hot);
      gl.uniform1f(su.uCur, s.cur); gl.uniform1f(su.uDev, s.dev);
      gl.uniform2f(su.uBox, c.box[0], c.box[1]);
      gl.uniform4f(su.uPrint, c.print[0], c.print[1], c.print[2], c.print[3]);
      gl.uniform1f(su.uRot, c.rot);
      gl.uniform3f(su.uLamp, c.lamp[0], c.lamp[1], c.lamp[2]);
      gl.uniform1i(su.uTex, 0);
      gl.uniform1f(su.uDirect, canAcc ? 0 : 1);
      gl.drawArrays(gl.TRIANGLES, 0, 3);
      if (canAcc && post) {
        gl.bindFramebuffer(gl.FRAMEBUFFER, null);
        gl.disable(gl.BLEND);
        gl.useProgram(post);
        gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, acc);
        gl.uniform1i(pu.uAcc, 1); gl.uniform2f(pu.uRes, W, H);
        gl.uniform1f(pu.uGrain, still ? 1 : s.frame % 64);
        gl.uniform2f(pu.uCentre, 0.45, c.wide ? 0.55 : 0.6);
        gl.drawArrays(gl.TRIANGLES, 0, 3);
      }
    };

    const go = (i: number, fromUser: boolean) => {
      const k = Math.max(0, Math.min(N - 1, i));
      if (fromUser) s.touched = true;
      s.target = k; s.want = k;
      if (k !== actRef.current) { actRef.current = k; setAct(k); }
      if (reduce) { s.sel = k; s.v = 0; s.cur = k; s.dev = 1; s.hot = 1; s.count = 0; }
      wake();
    };
    api.current = { go };

    const frame = (now: number) => {
      s.raf = 0;
      if (!s.visible) return;
      const dtRaw = s.last ? (now - s.last) / 1000 : 0.016;
      const dt = Math.min(0.033, dtRaw);
      s.last = now; s.frame++;
      if (!s.t0) { s.t0 = now; s.demoAt = now + 4200; }

      // the self-running demo: it opens on a finished print, holds, then walks the years until touched
      if (!still && !s.touched && now > s.demoAt) {
        const order = [3, 4, 5, 0, 1, 2];
        go(order[Math.floor((now - s.t0 - 4200) / 5200) % order.length] ?? START, false);
        s.demoAt = now + 5200;
      }

      let moving = false;
      if (!reduce) {
        const sub = 4, h = dt / sub;
        for (let k = 0; k < sub; k++) {
          // the strip has a little weight: it is pushed, coasts a hair past and settles
          const a = Math.max(-14, Math.min(14, 38 * (s.target - s.sel) - 9.5 * s.v));
          s.v += a * h; s.sel += s.v * h;
        }
        const far = Math.abs(s.target - s.sel);
        // the lamp under the glass dims while the strip is moving, then swells under the new frame
        const hotGoal = far < 0.04 && Math.abs(s.v) < 0.15 ? 1 : 0.15;
        s.hot += (hotGoal - s.hot) * (1 - Math.exp(-dt * (hotGoal > s.hot ? 3.2 : 9)));
        // the print: the old one clears to paper, then the new one comes up in the developer
        if (s.cur !== s.want) { s.dev = Math.max(0, s.dev - dt * 3.2); if (s.dev === 0) s.cur = s.want; }
        else if (s.hot > 0.5) s.dev = Math.min(1, s.dev + dt / 2.6);
        moving = Math.abs(s.v) > 0.002 || far > 0.0005 || Math.abs(s.hot - hotGoal) > 0.004 || s.dev < 1 || s.cur !== s.want;
        if (!moving) { s.sel = s.target; s.v = 0; }
      }
      if (!still) {
        s.sx += (s.px - s.sx) * (1 - Math.exp(-dt * 3)); s.sy += (s.py - s.sy) * (1 - Math.exp(-dt * 3));
        if (Math.abs(s.px - s.sx) > 0.002 || Math.abs(s.py - s.sy) > 0.002) moving = true;
      }

      const fast = Math.abs(s.v) > 0.25;
      if (s.fresh || !canAcc) { draw(1, 0); s.fresh = false; s.count = 1; }
      else if (moving) { draw(fast ? 0.7 : 0.35, 1 + (s.frame % 64)); s.count = 3; }
      else if (s.count < 48) { s.count++; draw(1 / s.count, s.count); }
      if (s.first) { s.first = false; setShown("gl"); }

      if (!still && dtRaw > 0.026) s.slow++; else s.slow = Math.max(0, s.slow - 2);
      if (s.slow > 45 && scale > 0.5) { scale *= 0.82; s.slow = 0; size(); }

      if (moving || s.count < 48 || (!still && !s.touched)) s.raf = requestAnimationFrame(frame);
    };
    const wake = () => { if (!s.raf && s.visible) { s.last = 0; s.raf = requestAnimationFrame(frame); } };

    size();
    placeTags(camera());
    const ro = new ResizeObserver(() => { size(); placeTags(camera()); wake(); });
    ro.observe(box);
    const io = new IntersectionObserver(([e]) => { s.visible = e.isIntersecting && !document.hidden; if (s.visible) wake(); }, { threshold: 0.05 });
    io.observe(box);
    const vis = () => { s.visible = !document.hidden; if (s.visible) wake(); };
    document.addEventListener("visibilitychange", vis);

    // A press on the strip brings that frame to the lamp.
    const down = (e: PointerEvent) => {
      if ((e.target as HTMLElement).closest("button")) return;
      const r = box.getBoundingClientRect(), c = camera();
      let best = -1, bd = 70;
      for (let i = 0; i < N; i++) {
        const [x, y] = project(c, [(i - s.sel) * PITCH, TOP, 0]);
        const d = Math.hypot(x - (e.clientX - r.left), (y - (e.clientY - r.top)) * 0.7);
        if (d < bd) { bd = d; best = i; }
      }
      if (best >= 0) go(best, true);
    };
    const move = (e: PointerEvent) => {
      if (still) return;
      const r = box.getBoundingClientRect();
      s.px = ((e.clientX - r.left) / r.width) * 2 - 1; s.py = ((e.clientY - r.top) / r.height) * 2 - 1;
      wake();
    };
    const leave = () => { s.px = 0; s.py = 0; wake(); };
    box.addEventListener("pointerdown", down);
    box.addEventListener("pointermove", move);
    box.addEventListener("pointerleave", leave);
    const lost = (e: Event) => { e.preventDefault(); setShown("still"); };
    cv.addEventListener("webglcontextlost", lost);
    wake();

    return () => {
      alive = false;
      cancelAnimationFrame(s.raf); ro.disconnect(); io.disconnect();
      document.removeEventListener("visibilitychange", vis);
      box.removeEventListener("pointerdown", down); box.removeEventListener("pointermove", move);
      box.removeEventListener("pointerleave", leave); cv.removeEventListener("webglcontextlost", lost);
      api.current = null;
    };
  }, []);

  const choose = (i: number) => {
    if (api.current) api.current.go(i, true); else { actRef.current = i; setAct(i); }
  };
  const keys = (e: React.KeyboardEvent, i: number) => {
    const next = e.key === "ArrowRight" || e.key === "ArrowDown" ? i + 1 : e.key === "ArrowLeft" || e.key === "ArrowUp" ? i - 1 : e.key === "Home" ? 0 : e.key === "End" ? N - 1 : -9;
    if (next === -9) return;
    e.preventDefault();
    const k = Math.max(0, Math.min(N - 1, next));
    choose(k);
    tags.current[k]?.focus();
  };
  const y = years[act];
  const fallback = shown === "still";

  return (
    <section className="relative isolate overflow-hidden bg-[#0c0a08] text-[#f3ebdf]" style={{ fontFamily: FONT }}>
      <div ref={stage} className="relative mx-auto h-[800px] w-full max-w-[1600px] sm:h-[820px] lg:h-[740px]" style={{ background: "radial-gradient(70% 60% at 45% 30%, #2a221a 0%, #120e0a 60%, #0a0806 100%)" }}>
        <canvas ref={canvas} aria-hidden="true" className={`absolute inset-0 h-full w-full transition-opacity duration-700 motion-reduce:transition-none ${shown === "gl" ? "opacity-100" : "opacity-0"}`} />
        {fallback && <Still act={act} />}
        {fallback && <img src={y.photo} alt="" aria-hidden="true" className="absolute bottom-[12%] left-[9%] hidden h-[200px] w-[300px] rotate-[-3deg] border-[8px] border-[#e8e3da] object-cover shadow-2xl lg:block" style={{ backgroundColor: y.tone }} />}

        <p className="pointer-events-none absolute left-6 top-6 text-[11px] font-medium uppercase tracking-[0.32em] text-[#f3ebdf]/60 sm:left-10 lg:left-14 lg:top-10">
          {COMPANY}<span className="mx-2 text-[#f3ebdf]/30">/</span><span className="hidden sm:inline">{STRAP}</span><span className="sm:hidden">Our story</span>
        </p>

        <div aria-live="polite" className="pointer-events-none absolute inset-x-0 bottom-0 px-6 pb-8 sm:px-10 lg:left-auto lg:right-0 lg:w-[46%] lg:pb-16 lg:pr-14">
          {fallback && <img src={y.photo} alt="" className="mb-5 h-[150px] w-[225px] -rotate-2 border-[6px] border-[#e8e3da] object-cover shadow-2xl lg:hidden" style={{ backgroundColor: y.tone }} />}
          <p className="text-[86px] leading-[0.8] tracking-[-0.04em] text-[#f6e7cf] tabular-nums sm:text-[112px] lg:text-[136px]" style={{ fontFamily: SERIF }}>{y.year}</p>
          <h2 className="mt-4 text-[26px] font-semibold leading-[1.02] tracking-[-0.035em] sm:text-[32px] lg:mt-6 lg:text-[38px]">{y.title}</h2>
          <p className="mt-3 max-w-[27rem] text-[16px] leading-snug text-[#f3ebdf]/72 lg:text-[18px]" style={{ fontFamily: SERIF }}>{y.body}</p>
        </div>

        <ol aria-label="Years in our history" className={`pointer-events-none ${fallback ? "absolute inset-x-4 top-[calc(14%+186px)] flex justify-center gap-0 sm:gap-3" : "absolute inset-0"}`}>
          {years.map((it, i) => {
            const on = i === act;
            return (
              <li key={it.year}>
                <button
                  ref={(el) => { tags.current[i] = el; }}
                  type="button"
                  onClick={() => choose(i)}
                  onKeyDown={(e) => keys(e, i)}
                  aria-current={on ? "step" : undefined}
                  aria-label={`${it.year}: ${it.title}`}
                  tabIndex={on ? 0 : -1}
                  className={`pointer-events-auto flex min-h-[40px] min-w-[44px] items-center justify-center rounded-full px-1.5 text-[12px] sm:px-2.5 sm:text-[13px] font-semibold tabular-nums tracking-[0.06em] outline-none transition-colors duration-300 focus-visible:ring-2 focus-visible:ring-[#f0b979] motion-reduce:transition-none ${fallback ? "relative" : "absolute left-0 top-0 opacity-0"} ${on ? "text-[#f6c48a]" : "text-[#f3ebdf]/50 hover:text-[#f3ebdf]"}`}
                  style={fallback ? { opacity: 1 } : undefined}
                >
                  <span className={`mr-1.5 inline-block h-1.5 w-1.5 rounded-full transition-colors duration-300 ${on ? "bg-[#f6c48a]" : "bg-[#f3ebdf]/25"}`} />
                  {it.year}
                </button>
              </li>
            );
          })}
        </ol>
      </div>
    </section>
  );
}
```
