{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "aspheric-turn-spiral",
  "title": "Aspheric Turn Spiral",
  "description": "A full-bleed hero of a lens blank mid single-point diamond turning, a spiral groove cutting from center to rim with pitch that tightens toward the edge the way a real aspheric surface curves.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/loud/aspheric-turn-spiral/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\n// ---------------------------------------------------------------------------\n// AsphericTurnSpiral — a full-bleed WebGL hero rendering a lens blank mid\n// single-point diamond turning: a spiral groove cut from center to rim by a\n// tool whose radial feed reverses smoothly rather than ever resetting.\n//\n// ONE HEIGHT FIELD, three contributions, evaluated per pixel and central-\n// differenced for a surface normal:\n//   (1) a shallow concave bowl (-BOWL_AMP*r^2), the lens's own aspheric sag\n//   (2) the spiral groove itself, cos(2π*(θ/2π + turns(r))) where turns(r) =\n//       N_BASE*r + N_ASPH*r^2 — the r^2 term is what makes the groove pitch\n//       tighten toward the rim the way a real aspheric surface's local slope\n//       increases with radius, not a uniform Archimedean spiral\n//   (3) a faint isotropic pre-cut texture on the still-blank annulus outside\n//       the tool's current radius, so \"already turned\" and \"not yet turned\"\n//       read as two different surfaces, not one disc with a groove painted\n//       partway across it\n//\n// THE FEED IS A TRIANGLE WAVE, never a reset: cutRadius(t) sweeps center→rim\n// over 45s then rim→center over the next 45s (FEED_PERIOD = 90s total), so\n// the tool is always mid-traverse — reversing direction is itself a real\n// process event (a verification or re-finishing pass), not fabricated\n// motion. The boundary between cut and uncut carries a bright edgeGlow term:\n// freshly-turned surface catches light hardest right at the tool's current\n// position, which is the slow (45s-scale) structural cue for \"alive at\n// rest.\"\n//\n// THE FAST, FOLLOWABLE cue is separate: the reflection environment's azimuth\n// is rotated by u_time * SPINDLE_RATE (0.08 rev/s, slowed hard from a real\n// SPDT spindle's ~600-3000rpm and documented here rather than driven 1:1 —\n// the round 9 decoupling rule), so a specular band sweeps around the groove\n// continuously. A second, higher-frequency azimuthal panel (4x the spindle\n// rate) buys a visible band crossing roughly every ~3s without needing the\n// base rotation itself to be that fast.\n//\n// MONOCHROME: five luminance stops derived from --background, --foreground,\n// --ns-muted and --border (getComputedStyle at mount, re-read on a\n// MutationObserver watching documentElement's class). No --ns-accent\n// anywhere in the shader — this is a full-bleed sheet like weld-pool, so the\n// ramp spans near-black to near-white in BOTH themes; only bias/contrast\n// move between themes, never direction. Density comes from the environment\n// having several narrow light sources at different elevations plus two\n// azimuthal panels, the same \"give the room structure, not the material\n// more noise\" approach as weld-pool, so an almost-flat patch of the bowl\n// still crosses multiple reflection bands.\n//\n// POINTER: a gentle parallax only — the pointer nudges the view direction\n// (±0.18 of view-space xy, eased over ~150ms), orbiting the specular\n// highlight across the groove. It never restarts the cut, changes the feed\n// direction, or touches spiral phase.\n// ---------------------------------------------------------------------------\n\nexport interface AsphericTurnSpiralProps {\n  /** Flow/feed speed multiplier. @default 1 */\n  speed?: number;\n  /** Feature scale — smaller reads as a tighter, more zoomed-in disc. @default 1 */\n  scale?: number;\n  /** How far the groove and bowl stand out of the surface, 0..1+. @default 1 */\n  relief?: number;\n  /** Freezes the surface on a composed still frame without unmounting. */\n  paused?: boolean;\n  /** Rendered in the DOM over the surface — eyebrow, subhead, CTA. */\n  children?: React.ReactNode;\n  className?: string;\n  style?: React.CSSProperties;\n}\n\nconst VERT_SRC = `\nattribute vec2 a_pos;\nvoid main() { gl_Position = vec4(a_pos, 0.0, 1.0); }\n`;\n\nconst FRAG_SRC = `\nprecision highp float;\n\nuniform vec2 u_size;   // css px\nuniform float u_dpr;\nuniform float u_time;\nuniform float u_scale;\nuniform float u_relief;\nuniform vec2 u_ptr;    // -1..1 eased pointer offset\nuniform vec3 u_c0;\nuniform vec3 u_c1;\nuniform vec3 u_c2;\nuniform vec3 u_c3;\nuniform vec3 u_c4;\nuniform float u_bias;\nuniform float u_contrast;\n\nconst float TWO_PI = 6.28318530718;\nconst float FEED_PERIOD = 90.0;\nconst float SPINDLE_RATE = 0.08; // rev/s, decoupled from a real ~600-3000rpm SPDT spindle\nconst float N_BASE = 14.0;\nconst float N_ASPH = 26.0;\nconst float BOWL_AMP = 0.55;\nconst float GROOVE_AMP = 0.11;\n\nfloat hash21(vec2 p) {\n  p = fract(p * vec2(287.13, 419.71));\n  p += dot(p, p + 27.31);\n  return fract(p.x * p.y);\n}\n\nfloat vnoise(vec2 p) {\n  vec2 i = floor(p);\n  vec2 f = fract(p);\n  float a = hash21(i);\n  float b = hash21(i + vec2(1.0, 0.0));\n  float c = hash21(i + vec2(0.0, 1.0));\n  float d = hash21(i + vec2(1.0, 1.0));\n  vec2 u = f * f * (3.0 - 2.0 * f);\n  return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\n// pitch tightens toward the rim: the r^2 term is the aspheric read\nfloat turns(float r) {\n  return N_BASE * r + N_ASPH * r * r;\n}\n\n// the tool's radial position: a triangle wave, never a reset. Direction\n// reversal at each end is a real process event, not fabricated motion.\nfloat cutRadius(float t) {\n  float ph = fract(t / FEED_PERIOD);\n  float tri = ph < 0.5 ? ph * 2.0 : 2.0 - ph * 2.0;\n  return mix(0.05, 1.04, tri);\n}\n\nfloat height(vec2 p, out float cutMask, out float edgeGlow, out float r) {\n  vec2 c = u_size * 0.5;\n  float ref = min(u_size.x, u_size.y);\n  float discR = ref * 0.46 * u_scale;\n  vec2 d = (p - c) / discR;\n  r = length(d);\n  float theta = atan(d.y, d.x);\n\n  float rc = cutRadius(u_time);\n  cutMask = smoothstep(rc + 0.018, rc - 0.018, r);\n  edgeGlow = exp(-pow((r - rc) / 0.022, 2.0));\n\n  float idx = theta / TWO_PI + turns(r);\n  float groove = cos(idx * TWO_PI);\n\n  float bowl = -BOWL_AMP * r * r;\n  // pre-cut blank texture, only where the tool has not yet passed\n  float blank = (vnoise(d * 9.0 + 4.1) - 0.5) * 0.05 * (1.0 - cutMask);\n\n  return bowl + GROOVE_AMP * groove * cutMask + blank;\n}\n\nfloat heightOnly(vec2 p) {\n  float m; float e; float rr;\n  return height(p, m, e, rr);\n}\n\nvec3 ramp(float x) {\n  vec3 c = mix(u_c0, u_c1, smoothstep(0.0, 0.27, x));\n  c = mix(c, u_c2, smoothstep(0.25, 0.55, x));\n  c = mix(c, u_c3, smoothstep(0.52, 0.83, x));\n  c = mix(c, u_c4, smoothstep(0.81, 1.0, x));\n  return c;\n}\n\nvoid main() {\n  vec2 p = vec2(gl_FragCoord.x, u_size.y * u_dpr - gl_FragCoord.y) / u_dpr;\n\n  float cutMask; float edgeGlow; float r;\n  float h0 = height(p, cutMask, edgeGlow, r);\n  float eps = 1.2;\n  float hx = heightOnly(p + vec2(eps, 0.0));\n  float hy = heightOnly(p + vec2(0.0, eps));\n\n  float k = u_relief / eps;\n  vec3 n = normalize(vec3(-(hx - h0) * k, (hy - h0) * k, 1.0));\n\n  vec2 c = u_size * 0.5;\n  float ref = min(u_size.x, u_size.y);\n  vec2 vp = (p - c) / ref;\n  vec3 v = normalize(vec3(vp.x * 0.5 + u_ptr.x * 0.18, -vp.y * 0.5 - u_ptr.y * 0.18, 1.0));\n  vec3 rf = reflect(-v, n);\n\n  float el = rf.y;\n  float az = atan(rf.x, rf.z) + u_time * SPINDLE_RATE * TWO_PI;\n\n  // an analytic achromatic studio: several narrow sources at different\n  // elevations plus two azimuthal panels, one at the spindle rate and one 4x\n  // faster, so a nearly-flat patch of the bowl still crosses several\n  // reflection bands and the frame does not read as pure horizon banding\n  float L = 0.40;\n  L += 0.22 * smoothstep(0.05, 0.62, el);\n  L -= 0.20 * smoothstep(0.0, -0.55, el);\n  L += 0.26 * exp(-pow((el - 0.16) / 0.075, 2.0));\n  L += 0.16 * exp(-pow((el + 0.28) / 0.10, 2.0));\n  L += 0.15 * exp(-pow(sin(az * 1.0) / 0.42, 2.0));\n  L += 0.09 * exp(-pow(sin(az * 4.0 + 1.3) / 0.24, 2.0));\n\n  float fres = pow(1.0 - max(dot(n, v), 0.0), 5.0);\n  L += fres * 0.10;\n\n  // freshly-turned surface catches light hardest right at the tool's edge\n  L += edgeGlow * 0.35;\n\n  vec3 l1 = normalize(vec3(-0.3, 0.65, 0.6));\n  float s1 = pow(max(dot(rf, l1), 0.0), 70.0);\n  L += s1 * 0.5;\n\n  // flat backdrop beyond the disc rim\n  float discEdge = smoothstep(1.0, 1.06, r);\n  L = mix(L, 0.30, discEdge);\n\n  float Lc = clamp((L - 0.5) * u_contrast + 0.5 + u_bias, 0.0, 1.0);\n  vec3 col = ramp(Lc);\n\n  float vig = smoothstep(0.9, 1.9, length(vp * vec2(1.0, 1.3)));\n  col = mix(col, u_c0, vig * 0.28);\n\n  gl_FragColor = vec4(col, 1.0);\n}\n`;\n\ntype RGB = [number, number, number];\n\nfunction parseHex(raw: string): RGB | null {\n  const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(raw.trim());\n  if (!m) return null;\n  let h = m[1];\n  if (h.length === 3) h = h.split(\"\").map((c) => c + c).join(\"\");\n  const n = parseInt(h, 16);\n  return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];\n}\n\nfunction mixRGB(a: RGB, b: RGB, t: number): RGB {\n  return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];\n}\n\nfunction luminance([r, g, b]: RGB): number {\n  return 0.2126 * r + 0.7152 * g + 0.0722 * b;\n}\n\nfunction compile(gl: WebGLRenderingContext, type: number, src: string): WebGLShader {\n  const s = gl.createShader(type)!;\n  gl.shaderSource(s, src);\n  gl.compileShader(s);\n  if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {\n    const info = gl.getShaderInfoLog(s);\n    gl.deleteShader(s);\n    throw new Error(`aspheric-turn-spiral: shader compile failed: ${info ?? \"\"}`);\n  }\n  return s;\n}\n\n// A time offset where the feed is 60% along its outward leg (ph = t/90 =\n// 0.3, tri = 0.6) — the tool boundary and its edgeGlow are clearly inside\n// the frame with maximum visible groove density behind it, the most\n// structured single frame rather than whatever t=0 happens to be.\nconst STATIC_TIME = 27;\nconst STATIC_LABEL = \"spiral-60pct\";\n\nexport function AsphericTurnSpiral({\n  speed = 1,\n  scale = 1,\n  relief = 1,\n  paused = false,\n  children,\n  className = \"\",\n  style,\n}: AsphericTurnSpiralProps) {\n  const wrapRef = useRef<HTMLDivElement | null>(null);\n  const canvasRef = useRef<HTMLCanvasElement | null>(null);\n  const pausedRef = useRef(paused);\n  pausedRef.current = paused;\n\n  useEffect(() => {\n    const wrap = wrapRef.current;\n    const canvas = canvasRef.current;\n    if (!wrap || !canvas) return;\n\n    let gl: WebGLRenderingContext | null = null;\n    let program: WebGLProgram | null = null;\n    let vs: WebGLShader | null = null;\n    let fs: WebGLShader | null = null;\n    let buffer: WebGLBuffer | null = null;\n    const locs = new Map<string, WebGLUniformLocation | null>();\n\n    let raf = 0;\n    let running = false;\n    let staticMode = false;\n    let disposed = false;\n    let cssW = 0;\n    let cssH = 0;\n    let dpr = 1;\n    let lastMs = performance.now();\n    let simTime = 0;\n\n    // Adaptive render scale, insurance rather than the fix: steps down only\n    // after a sustained (~900ms) stretch over budget, steps back up only\n    // after a much longer clean stretch, doubling the wait on each failure —\n    // see weld-pool's component.tsx for the full reasoning this mirrors.\n    const SCALES = [1, 0.75, 0.55];\n    const BUDGET_OVER = 24;\n    let scaleIdx = 0;\n    let frameEma = 16.7;\n    let overMs = 0;\n    let underMs = 0;\n    let upWindow = 8000;\n\n    let ptrTgtX = 0;\n    let ptrTgtY = 0;\n    let ptrX = 0;\n    let ptrY = 0;\n\n    let c0: RGB = [0.03, 0.03, 0.03];\n    let c1: RGB = [0.18, 0.18, 0.18];\n    let c2: RGB = [0.56, 0.56, 0.56];\n    let c3: RGB = [0.93, 0.93, 0.93];\n    let c4: RGB = [1, 1, 1];\n    let bias = 0;\n    let contrast = 1.15;\n\n    // Five stops spanning near-black to near-white in BOTH themes: a\n    // full-bleed sheet is the page, so it never inverts direction — only the\n    // distribution (bias/contrast) moves between themes.\n    const readColors = () => {\n      const cs = getComputedStyle(document.documentElement);\n      const bg = parseHex(cs.getPropertyValue(\"--background\")) ?? [1, 1, 1];\n      const fg = parseHex(cs.getPropertyValue(\"--foreground\")) ?? [0.09, 0.09, 0.09];\n      const muted = parseHex(cs.getPropertyValue(\"--ns-muted\")) ?? [0.55, 0.55, 0.55];\n      const border = parseHex(cs.getPropertyValue(\"--border\")) ?? [0.18, 0.18, 0.18];\n      const black: RGB = [0, 0, 0];\n      const white: RGB = [1, 1, 1];\n      if (luminance(bg) < 0.5) {\n        c0 = mixRGB(bg, black, 0.55);\n        c1 = mixRGB(border, bg, 0.15);\n        c2 = muted;\n        c3 = fg;\n        c4 = mixRGB(fg, white, 0.85);\n        bias = -0.08;\n        contrast = 1.15;\n      } else {\n        c0 = mixRGB(fg, black, 0.35);\n        c1 = mixRGB(fg, muted, 0.55);\n        c2 = mixRGB(muted, bg, 0.6);\n        c3 = mixRGB(bg, muted, 0.16);\n        c4 = bg;\n        bias = 0.02;\n        contrast = 1.2;\n      }\n    };\n    // no paint before this first read\n    readColors();\n\n    const loc = (name: string): WebGLUniformLocation | null => {\n      if (!locs.has(name)) locs.set(name, gl!.getUniformLocation(program!, name));\n      return locs.get(name) ?? null;\n    };\n\n    const setup = (): boolean => {\n      gl = canvas.getContext(\"webgl\", {\n        alpha: false,\n        antialias: false,\n        premultipliedAlpha: false,\n        powerPreference: \"high-performance\",\n      }) as WebGLRenderingContext | null;\n      if (!gl) return false;\n      try {\n        vs = compile(gl, gl.VERTEX_SHADER, VERT_SRC);\n        fs = compile(gl, gl.FRAGMENT_SHADER, FRAG_SRC);\n        const p = gl.createProgram();\n        if (!p) return false;\n        program = p;\n        gl.attachShader(p, vs);\n        gl.attachShader(p, fs);\n        gl.linkProgram(p);\n        if (!gl.getProgramParameter(p, gl.LINK_STATUS)) return false;\n      } catch {\n        return false;\n      }\n      gl.useProgram(program);\n      buffer = gl.createBuffer();\n      gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n      gl.bufferData(\n        gl.ARRAY_BUFFER,\n        new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]),\n        gl.STATIC_DRAW\n      );\n      const aLoc = gl.getAttribLocation(program, \"a_pos\");\n      gl.enableVertexAttribArray(aLoc);\n      gl.vertexAttribPointer(aLoc, 2, gl.FLOAT, false, 0, 0);\n      locs.clear();\n      return true;\n    };\n\n    const teardown = () => {\n      if (!gl) return;\n      if (buffer) gl.deleteBuffer(buffer);\n      if (program) gl.deleteProgram(program);\n      if (vs) gl.deleteShader(vs);\n      if (fs) gl.deleteShader(fs);\n      buffer = program = vs = fs = null;\n      gl = null;\n    };\n\n    const draw = () => {\n      if (!gl || !program || cssW <= 0 || cssH <= 0) return;\n      const t = staticMode ? STATIC_TIME : simTime;\n      gl.uniform2f(loc(\"u_size\"), cssW, cssH);\n      gl.uniform1f(loc(\"u_dpr\"), dpr);\n      gl.uniform1f(loc(\"u_time\"), t);\n      gl.uniform1f(loc(\"u_scale\"), Math.max(0.2, scale));\n      gl.uniform1f(loc(\"u_relief\"), 10 * Math.max(0, relief));\n      gl.uniform2f(loc(\"u_ptr\"), ptrX, ptrY);\n      gl.uniform3f(loc(\"u_c0\"), c0[0], c0[1], c0[2]);\n      gl.uniform3f(loc(\"u_c1\"), c1[0], c1[1], c1[2]);\n      gl.uniform3f(loc(\"u_c2\"), c2[0], c2[1], c2[2]);\n      gl.uniform3f(loc(\"u_c3\"), c3[0], c3[1], c3[2]);\n      gl.uniform3f(loc(\"u_c4\"), c4[0], c4[1], c4[2]);\n      gl.uniform1f(loc(\"u_bias\"), bias);\n      gl.uniform1f(loc(\"u_contrast\"), contrast);\n      gl.viewport(0, 0, canvas.width, canvas.height);\n      gl.drawArrays(gl.TRIANGLES, 0, 6);\n    };\n\n    const loop = (nowMs: number) => {\n      const rawMs = nowMs - lastMs;\n      const dt = Math.min(0.05, Math.max(0, rawMs / 1000));\n      lastMs = nowMs;\n      simTime += dt * speed;\n      const pk = 1 - Math.exp(-dt / 0.15);\n      ptrX += (ptrTgtX - ptrX) * pk;\n      ptrY += (ptrTgtY - ptrY) * pk;\n      draw();\n\n      const clamped = Math.min(50, rawMs);\n      frameEma += (clamped - frameEma) * (1 - Math.exp(-clamped / 120));\n      if (frameEma > BUDGET_OVER) {\n        overMs += clamped;\n        underMs = 0;\n      } else {\n        underMs += clamped;\n        overMs = 0;\n      }\n      const down = overMs > 900 && scaleIdx < SCALES.length - 1;\n      const up = underMs > upWindow && scaleIdx > 0;\n      if (down || up) {\n        scaleIdx += down ? 1 : -1;\n        if (down) upWindow = Math.min(64000, upWindow * 2);\n        overMs = 0;\n        underMs = 0;\n        frameEma = 16.7;\n        applyBacking();\n      }\n      raf = requestAnimationFrame(loop);\n    };\n    const wake = () => {\n      if (running || disposed) return;\n      running = true;\n      lastMs = performance.now();\n      raf = requestAnimationFrame(loop);\n    };\n    const sleep = () => {\n      cancelAnimationFrame(raf);\n      running = false;\n    };\n\n    // DPR capped at 1.5, the same full-bleed convention as weld-pool: the\n    // area term dominates cost here too, and the shader is cheap enough\n    // (three height evaluations/pixel) that 1.5 holds resolution comfortably.\n    const applyBacking = () => {\n      if (cssW < 2 || cssH < 2) return;\n      dpr = Math.min(window.devicePixelRatio || 1, 1.5) * SCALES[scaleIdx];\n      const pw = Math.round(cssW * dpr);\n      const ph = Math.round(cssH * dpr);\n      if (canvas.width !== pw || canvas.height !== ph) {\n        canvas.width = pw;\n        canvas.height = ph;\n      }\n      canvas.style.width = `${cssW}px`;\n      canvas.style.height = `${cssH}px`;\n      draw();\n    };\n\n    const resize = () => {\n      const rect = wrap.getBoundingClientRect();\n      if (rect.width < 2 || rect.height < 2) return;\n      cssW = rect.width;\n      cssH = rect.height;\n      scaleIdx = 0;\n      overMs = 0;\n      underMs = 0;\n      upWindow = 8000;\n      frameEma = 16.7;\n      applyBacking();\n    };\n\n    const onPointerMove = (e: PointerEvent) => {\n      const rect = wrap.getBoundingClientRect();\n      ptrTgtX = ((e.clientX - rect.left) / Math.max(1, rect.width) - 0.5) * 2;\n      ptrTgtY = ((e.clientY - rect.top) / Math.max(1, rect.height) - 0.5) * 2;\n      if (staticMode) draw();\n    };\n    const onPointerLeave = () => {\n      ptrTgtX = 0;\n      ptrTgtY = 0;\n      if (staticMode) draw();\n    };\n\n    if (!setup()) return; // no WebGL: children still render over the page bg\n    const ro = new ResizeObserver(resize);\n    ro.observe(wrap);\n    resize();\n\n    wrap.addEventListener(\"pointermove\", onPointerMove);\n    wrap.addEventListener(\"pointerleave\", onPointerLeave);\n\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    let reduced = mq.matches;\n    const applyMode = () => {\n      if (reduced || pausedRef.current) {\n        staticMode = true;\n        sleep();\n        draw();\n      } else {\n        staticMode = false;\n        wake();\n      }\n    };\n    const onMq = () => {\n      reduced = mq.matches;\n      applyMode();\n    };\n    mq.addEventListener(\"change\", onMq);\n\n    let onScreen = true;\n    const io = new IntersectionObserver(\n      (entries) => {\n        onScreen = entries.some((en) => en.isIntersecting);\n        if (!onScreen) sleep();\n        else if (!staticMode && !document.hidden) wake();\n      },\n      { threshold: 0 }\n    );\n    io.observe(wrap);\n\n    const onVis = () => {\n      if (document.hidden) sleep();\n      else if (!staticMode && onScreen) wake();\n    };\n    document.addEventListener(\"visibilitychange\", onVis);\n    applyMode();\n\n    let lastPolledPaused = pausedRef.current;\n    let poll = 0;\n    const tick = () => {\n      if (pausedRef.current !== lastPolledPaused) {\n        lastPolledPaused = pausedRef.current;\n        applyMode();\n      }\n      poll = window.setTimeout(tick, 140);\n    };\n    tick();\n\n    const themeObserver = new MutationObserver(() => {\n      readColors();\n      if (staticMode) draw();\n    });\n    themeObserver.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\"],\n    });\n\n    const onLost = (e: Event) => {\n      e.preventDefault();\n      sleep();\n    };\n    const onRestored = () => {\n      if (setup()) {\n        resize();\n        applyMode();\n      }\n    };\n    canvas.addEventListener(\"webglcontextlost\", onLost);\n    canvas.addEventListener(\"webglcontextrestored\", onRestored);\n\n    return () => {\n      disposed = true;\n      ro.disconnect();\n      io.disconnect();\n      mq.removeEventListener(\"change\", onMq);\n      document.removeEventListener(\"visibilitychange\", onVis);\n      themeObserver.disconnect();\n      canvas.removeEventListener(\"webglcontextlost\", onLost);\n      canvas.removeEventListener(\"webglcontextrestored\", onRestored);\n      wrap.removeEventListener(\"pointermove\", onPointerMove);\n      wrap.removeEventListener(\"pointerleave\", onPointerLeave);\n      window.clearTimeout(poll);\n      sleep();\n      teardown();\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [scale, relief]);\n\n  return (\n    <div\n      ref={wrapRef}\n      className={`relative isolate h-full w-full overflow-hidden bg-background ${className}`}\n      style={style}\n      data-static-frame={STATIC_LABEL}\n    >\n      <canvas ref={canvasRef} aria-hidden=\"true\" className=\"absolute inset-0 block h-full w-full\" />\n      {children ? <div className=\"relative z-[1] h-full w-full\">{children}</div> : null}\n    </div>\n  );\n}\n\nAsphericTurnSpiral.displayName = \"AsphericTurnSpiral\";\n",
      "type": "registry:ui",
      "target": "components/ui/aspheric-turn-spiral.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)",
      "color-ns-accent": "var(--ns-accent)"
    },
    "light": {
      "ns-muted": "#4d4d4d",
      "ns-accent": "#006bff"
    },
    "dark": {
      "ns-muted": "#8f8f8f"
    }
  },
  "meta": {
    "collection": "loud",
    "tags": [
      "hero",
      "canvas",
      "webgl",
      "shader",
      "lens",
      "metal",
      "cursor"
    ],
    "instruction": "Build <AsphericTurnSpiral speed? scale? relief? paused? children? className? style?> as a full-bleed WebGL hero rendering a lens surface being cut on a diamond-turning lathe. ONE HEIGHT FIELD evaluated per pixel and central-differenced (eps 1.2px, relief scaled by 10 * relief prop) for one surface normal, three summed contributions: (1) a shallow concave bowl, -BOWL_AMP*r^2 with BOWL_AMP 0.55 and r the polar radius normalized to a disc of radius 0.46*min(w,h)*scale, the lens's own aspheric sag; (2) the spiral groove itself, cos(2π*(θ/2π + turns(r))) where turns(r) = N_BASE*r + N_ASPH*r^2, N_BASE 14, N_ASPH 26 — the r^2 term is what makes the groove pitch tighten toward the rim, the whole reason this reads as an aspheric surface rather than a flat Archimedean spiral, at GROOVE_AMP 0.11; (3) a faint isotropic pre-cut texture, (vnoise(d*9+4.1)-0.5)*0.05, gated to only the annulus the tool has not yet reached, so cut and uncut read as two different surfaces rather than one disc with a groove drawn partway across it. THE FEED IS A TRIANGLE WAVE, never a reset: cutRadius(t) = mix(0.05, 1.04, tri(fract(t/90))) where tri is a 0→1→0 triangle over the 90s FEED_PERIOD (45s outward, 45s inward) — the tool is always mid-traverse, and a direction reversal at either end is a real process event (a verification or re-finishing pass), not fabricated motion. cutMask = smoothstep(rc+0.018, rc-0.018, r) gates the groove and blank texture; edgeGlow = exp(-((r-rc)/0.022)^2) is a separate bright term added straight into the lighting (0.35x) representing freshly-turned surface catching light hardest right at the tool's current position — this is the slow (45s-scale) structural cue for 'alive at rest.' THE FAST, FOLLOWABLE cue is decoupled from that: the reflection environment's azimuth is rotated by u_time * SPINDLE_RATE * 2π with SPINDLE_RATE 0.08 rev/s (slowed hard from a real SPDT spindle's ~600-3000rpm and documented here explicitly rather than driven 1:1, the round 9 decoupling rule), plus a second azimuthal panel at 4x that rate so a visible specular band crosses roughly every ~3s without the base rotation itself needing to be that fast. MONOCHROME STUDIO: reflection vector rf = reflect(-v, n) is looked up in an analytic achromatic room — sky/floor smoothsteps, two narrow elevation strips (exp(-((el-0.16)/0.075)^2) and exp(-((el+0.28)/0.10)^2)), the two azimuthal panels above, a fresnel rim (pow(1-dot(n,v),5) * 0.10) and one drifting specular lobe (exponent 70, weight 0.5) — several narrow sources so a nearly-flat patch of the bowl still crosses multiple reflection bands, the same 'give the room structure, not the material more noise' approach as weld-pool. Palette: five stops from --background, --foreground, --ns-muted and --border via getComputedStyle at mount, re-read on a documentElement class MutationObserver, spanning near-black to near-white in BOTH themes since this is a full-bleed sheet like weld-pool — only bias/contrast move between themes (dark: bias -0.08, contrast 1.15; light: bias 0.02, contrast 1.2), never direction, and no --ns-accent anywhere in the shader. POINTER is a gentle parallax only: pointer position eases into u_ptr over a 150ms time constant and nudges the view direction by ±0.18 in view-space xy, orbiting the specular highlight across the groove; it never restarts the cut, changes feed direction, or touches spiral phase, and it still redraws the frozen frame under prefers-reduced-motion or paused. HOST: DPR capped at 1.5 (full-bleed area cost dominates, matching weld-pool's convention), ResizeObserver on the wrapper, an adaptive render scale ladder (1/0.75/0.55, EMA frame time, steps down after ~900ms sustained over a 24ms budget, steps up only after a much longer clean stretch that doubles per failure), IntersectionObserver (threshold 0) and visibilitychange both pausing the single rAF loop, and webglcontextlost/restored rebuilding cleanly. prefers-reduced-motion (and paused) draw exactly one composed still frame at STATIC_TIME=27s, named 'spiral-60pct' (feed 60% along its outward leg, ph=0.3 so tri=0.6) — maximum visible groove density with the tool boundary and its edgeGlow clearly inside the frame, never a blank grey box. Children render over the surface for an eyebrow, subhead and CTA; because the surface spans the full value range in both themes they need a token scrim (bg-background/70 + backdrop-blur) rather than bare type."
  },
  "type": "registry:ui"
}