{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ogl-canvas",
  "type": "registry:lib",
  "title": "OGL Canvas",
  "description": "A reusable client harness for fullscreen fragment-shader backgrounds. Manages uTime/uResolution/uPointer uniforms, clamps DPR, pauses offscreen and on hidden tabs, freezes under reduced motion, and fully tears down WebGL on unmount.",
  "dependencies": [
    "ogl"
  ],
  "registryDependencies": [
    "https://crucibleui.com/r/use-visibility-pause.json",
    "https://crucibleui.com/r/use-reduced-motion.json",
    "https://crucibleui.com/r/use-dpr-cap.json"
  ],
  "files": [
    {
      "path": "registry/default/lib/ogl-canvas/ogl-canvas.tsx",
      "type": "registry:lib",
      "target": "lib/ogl-canvas.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Mesh, Program, Renderer, Triangle } from \"ogl\";\n\nimport { cn } from \"@/lib/utils\";\nimport { clampDpr, useDprCap } from \"@/hooks/use-dpr-cap\";\nimport { useReducedMotion } from \"@/hooks/use-reduced-motion\";\nimport { useVisibilityPause } from \"@/hooks/use-visibility-pause\";\n\n/** A single OGL uniform. Mirrors OGL's `{ value }` convention. */\nexport interface Uniform<T = unknown> {\n  value: T;\n}\n\n/** Caller-supplied uniform map, merged over the auto-managed uniforms. */\nexport type UniformMap = Record<string, Uniform>;\n\nexport interface OglCanvasProps extends React.ComponentPropsWithoutRef<\"div\"> {\n  /**\n   * GLSL ES 1.00 fragment shader source. Must declare `precision` and write\n   * `gl_FragColor`. Receives the auto-managed uniforms plus anything in\n   * `uniforms`, and the `vUv` varying (0–1 across the canvas).\n   */\n  fragment: string;\n  /**\n   * Optional vertex shader override. Defaults to a fullscreen-triangle\n   * pass-through that forwards `uv` as `vUv`. Rarely needed.\n   */\n  vertex?: string;\n  /**\n   * Caller uniforms merged over the built-ins. Values are read on every commit,\n   * so passing a fresh object with updated `.value`s live-updates the shader.\n   * Reserved names `uTime`, `uResolution`, `uPointer`, `uPointerActive` are managed for you.\n   */\n  uniforms?: UniformMap;\n  /**\n   * devicePixelRatio ceiling. The effective DPR is\n   * `min(window.devicePixelRatio, dpr)`. Default 2.\n   */\n  dpr?: number;\n  /** Freeze the RAF loop, holding the last rendered frame. */\n  paused?: boolean;\n  /**\n   * Fired once the GL context is live and the FIRST frame has been rendered —\n   * use it to fade the canvas in from a placeholder so the shader never pops in\n   * suddenly after its chunk loads and initializes.\n   */\n  onReady?: () => void;\n  /**\n   * `uTime` (seconds) used for the single static frame rendered when the user\n   * prefers reduced motion. Pick a value that looks good frozen. Default 4.\n   */\n  reducedMotionTime?: number;\n  /**\n   * Idle autoplay. When truthy, a ghost pointer takes over after ~1.5s with no\n   * real pointer, drifting on a slow seeded Lissajous path so pointer-reactive\n   * shaders never look dead in thumbnails and heroes; the real pointer takes\n   * over instantly. `true` = presence 0.75 (fed to `uPointerActive`), or pass a\n   * 0–1 number. Off by default so existing consumers are unchanged.\n   */\n  ghost?: boolean | number;\n}\n\n/** Ghost takes over this long after the last real pointer movement. */\nconst GHOST_DELAY_MS = 1500;\n\nconst DEFAULT_VERTEX = /* glsl */ `\nprecision highp float;\nattribute vec2 position;\nattribute vec2 uv;\nvarying vec2 vUv;\nvoid main() {\n  vUv = uv;\n  gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\ninterface Internals {\n  renderer: Renderer;\n  mesh: Mesh;\n  program: Program;\n  uTime: Uniform<number>;\n  uResolution: Uniform<[number, number]>;\n  uPointer: Uniform<[number, number]>;\n  /** Eased 0–1 presence of a real pointer over the canvas (0 once it leaves). */\n  uPointerActive: Uniform<number>;\n  /** Un-smoothed target the pointer lerps toward. */\n  pointerTarget: [number, number];\n  /** Un-smoothed target for `uPointerActive`. */\n  pointerActiveTarget: number;\n  loseContext: (() => void) | null;\n}\n\n/**\n * OGL's Program.setShaders early-returns on a failed link WITHOUT creating\n * `uniformLocations` — rendering such a program throws deep inside OGL. Treat\n * \"unlinked\" as \"skip the frame\": the GLSL warning is already in the console.\n */\nfunction programLinked(program: Program): boolean {\n  return (program as unknown as { uniformLocations?: unknown }).uniformLocations !== undefined;\n}\n\n/** Write caller uniform values into the live program, creating missing keys. */\nfunction syncUniforms(program: Program, uniforms: UniformMap | undefined) {\n  if (!uniforms) return;\n  for (const key in uniforms) {\n    const incoming = uniforms[key];\n    const existing = program.uniforms[key];\n    if (existing) existing.value = incoming.value;\n    else program.uniforms[key] = { value: incoming.value };\n  }\n}\n\n/**\n * ogl-canvas — a reusable client harness for fullscreen fragment-shader\n * backgrounds. Runs a single fullscreen triangle through your shader, manages\n * `uTime` / `uResolution` / `uPointer` / `uPointerActive` (eased 0–1 pointer presence) uniforms, clamps DPR, pauses offscreen\n * and on hidden tabs, and freezes at one frame under prefers-reduced-motion.\n * Full WebGL teardown on unmount.\n *\n * Build a background on top of it: supply a `fragment` and any `uniforms`.\n */\nexport function OglCanvas({\n  fragment,\n  vertex = DEFAULT_VERTEX,\n  uniforms,\n  dpr = 2,\n  paused = false,\n  reducedMotionTime = 4,\n  ghost = false,\n  onReady,\n  className,\n  ...rest\n}: OglCanvasProps) {\n  const internalsRef = React.useRef<Internals | null>(null);\n  const [ready, setReady] = React.useState(false);\n  // Latest onReady, called from the setup effect without re-triggering GL init.\n  const onReadyRef = React.useRef(onReady);\n  onReadyRef.current = onReady;\n  // GL init can fail outright (context cap reached, blocked WebGL) and a live\n  // context can be evicted later — both degrade to a static ember wash rather\n  // than a crash or a black hole.\n  const [failed, setFailed] = React.useState(false);\n\n  const reducedMotion = useReducedMotion();\n  const ghostStrength = ghost === true ? 0.75 : typeof ghost === \"number\" ? Math.max(0, Math.min(1, ghost)) : 0;\n  const ghostRef = React.useRef(ghostStrength);\n  ghostRef.current = ghostStrength;\n  // performance.now() of the last real pointer activity; -Infinity = never.\n  const lastRealPointerRef = React.useRef(-Infinity);\n  // A real pointer resting over the canvas (even motionless) keeps the ghost away.\n  const hoveringRef = React.useRef(false);\n  // Latest caller uniforms, read inside the setup effect without re-triggering it.\n  const uniformsRef = React.useRef(uniforms);\n  uniformsRef.current = uniforms;\n\n  // useVisibilityPause restarts its clock at 0 whenever its `paused` option\n  // flips. Fold the time already shown into a base offset on every flip so\n  // un-pausing resumes the shader where it froze instead of snapping to t=0.\n  const loopPaused = paused || reducedMotion;\n  const timeBaseRef = React.useRef(0);\n  const lastElapsedRef = React.useRef(0);\n  const prevLoopPausedRef = React.useRef(loopPaused);\n  if (prevLoopPausedRef.current !== loopPaused) {\n    prevLoopPausedRef.current = loopPaused;\n    timeBaseRef.current += lastElapsedRef.current;\n    lastElapsedRef.current = 0;\n  }\n\n  const tick = React.useCallback((elapsed: number) => {\n    const it = internalsRef.current;\n    if (!it || !programLinked(it.program)) return;\n    lastElapsedRef.current = elapsed;\n    const p = it.uPointer.value;\n    const t = it.pointerTarget;\n    const g = ghostRef.current;\n    if (g > 0 && !hoveringRef.current && performance.now() - lastRealPointerRef.current > GHOST_DELAY_MS) {\n      // Slow, non-repeating-looking drift that stays clear of the edges.\n      const gt = timeBaseRef.current + elapsed;\n      t[0] = 0.5 + 0.32 * Math.sin(gt * 0.23) * Math.cos(gt * 0.071);\n      t[1] = 0.5 + 0.26 * Math.sin(gt * 0.17 + 1.3);\n      it.pointerActiveTarget = g;\n    }\n    // Organic easing toward the real pointer.\n    p[0] += (t[0] - p[0]) * 0.06;\n    p[1] += (t[1] - p[1]) * 0.06;\n    it.uPointerActive.value += (it.pointerActiveTarget - it.uPointerActive.value) * 0.06;\n    it.uTime.value = timeBaseRef.current + elapsed;\n    it.renderer.render({ scene: it.mesh });\n  }, []);\n\n  // The container is both the RAF gate (IntersectionObserver, via the hook) and\n  // the ResizeObserver target. The loop is suppressed while paused or reduced.\n  const containerRef = useVisibilityPause<HTMLDivElement>(tick, {\n    paused: loopPaused,\n  });\n\n  // Host-imposed DPR ceiling (galleries of thumbnails cap fill rate).\n  const dprCap = useDprCap();\n\n  // Setup / teardown. Rebuilds only when the shader program or DPR changes.\n  React.useEffect(() => {\n    const container = containerRef.current;\n    if (!container) return;\n\n    // The canvas is created per mount, never reused: once WEBGL_lose_context\n    // has killed a canvas's context (our teardown), that canvas can only ever\n    // return the same dead context again — under StrictMode's double-mount a\n    // reused JSX canvas would come back permanently black and OGL would crash\n    // rendering an unlinked program.\n    const canvas = document.createElement(\"canvas\");\n    canvas.setAttribute(\"aria-hidden\", \"true\");\n    canvas.className = \"absolute inset-0 block h-full w-full\";\n    container.appendChild(canvas);\n\n    const effectiveDpr = clampDpr(dprCap, dpr);\n\n    let renderer: Renderer;\n    let gl: Renderer[\"gl\"];\n    try {\n      renderer = new Renderer({\n        canvas,\n        dpr: effectiveDpr,\n        alpha: true,\n        antialias: false, // fullscreen fragment pass — MSAA buys nothing\n        premultipliedAlpha: false,\n      });\n      gl = renderer.gl;\n      gl.clearColor(0, 0, 0, 0);\n    } catch (err) {\n      console.warn(\"[crucible] WebGL unavailable — rendering static fallback.\", err);\n      canvas.remove();\n      setFailed(true);\n      return;\n    }\n\n    const onContextLost = (e: Event) => {\n      e.preventDefault();\n      setFailed(true);\n    };\n    canvas.addEventListener(\"webglcontextlost\", onContextLost);\n\n    const uTime: Uniform<number> = { value: 0 };\n    const uResolution: Uniform<[number, number]> = {\n      value: [gl.drawingBufferWidth || 1, gl.drawingBufferHeight || 1],\n    };\n    const uPointer: Uniform<[number, number]> = { value: [0.5, 0.5] };\n    // Shaders that react to the pointer should scale that reaction by this, so\n    // the effect releases when the pointer leaves instead of parking at center.\n    const uPointerActive: Uniform<number> = { value: 0 };\n\n    const program = new Program(gl, {\n      vertex,\n      fragment,\n      uniforms: {\n        uTime,\n        uResolution,\n        uPointer,\n        uPointerActive,\n        ...(uniformsRef.current ?? {}),\n      },\n    });\n\n    const geometry = new Triangle(gl);\n    const mesh = new Mesh(gl, { geometry, program });\n\n    const loseExt = gl.getExtension(\"WEBGL_lose_context\");\n\n    const internals: Internals = {\n      renderer,\n      mesh,\n      program,\n      uTime,\n      uResolution,\n      uPointer,\n      uPointerActive,\n      pointerTarget: [0.5, 0.5],\n      pointerActiveTarget: 0,\n      loseContext: loseExt ? () => loseExt.loseContext() : null,\n    };\n    internalsRef.current = internals;\n\n    const renderStatic = () => {\n      if (!programLinked(program)) return;\n      uTime.value = reducedMotion ? reducedMotionTime : uTime.value;\n      renderer.render({ scene: mesh });\n    };\n\n    // setSize reallocates the drawing buffer — skip no-op calls (the observer's\n    // initial callback repeats the size the synchronous mount call just set).\n    let sizedW = -1;\n    let sizedH = -1;\n    const resize = () => {\n      const { clientWidth: w, clientHeight: h } = container;\n      if (w === sizedW && h === sizedH) return;\n      sizedW = w;\n      sizedH = h;\n      renderer.setSize(Math.max(w, 1), Math.max(h, 1));\n      uResolution.value[0] = gl.drawingBufferWidth;\n      uResolution.value[1] = gl.drawingBufferHeight;\n      // Keep the frame correct even while the loop is idle (paused/reduced).\n      if (programLinked(program)) renderer.render({ scene: mesh });\n    };\n\n    const ro = new ResizeObserver(resize);\n    ro.observe(container);\n    resize();\n    renderStatic();\n\n    const onPointerMove = (e: PointerEvent) => {\n      const r = container.getBoundingClientRect();\n      if (r.width === 0 || r.height === 0) return;\n      internals.pointerTarget[0] = (e.clientX - r.left) / r.width;\n      // Flip Y so \"up\" on screen is 1.\n      internals.pointerTarget[1] = 1 - (e.clientY - r.top) / r.height;\n      internals.pointerActiveTarget = 1;\n      lastRealPointerRef.current = performance.now();\n      hoveringRef.current = true;\n    };\n    const onPointerLeave = () => {\n      internals.pointerActiveTarget = 0;\n      hoveringRef.current = false;\n      // Let the ghost resume after the usual delay rather than snapping in.\n      lastRealPointerRef.current = performance.now();\n      internals.pointerTarget[0] = 0.5;\n      internals.pointerTarget[1] = 0.5;\n    };\n    container.addEventListener(\"pointermove\", onPointerMove);\n    container.addEventListener(\"pointerleave\", onPointerLeave);\n\n    setReady(true);\n    // The first frame is on screen — let the caller fade the canvas in.\n    onReadyRef.current?.();\n\n    return () => {\n      setReady(false);\n      internalsRef.current = null;\n      ro.disconnect();\n      canvas.removeEventListener(\"webglcontextlost\", onContextLost);\n      container.removeEventListener(\"pointermove\", onPointerMove);\n      container.removeEventListener(\"pointerleave\", onPointerLeave);\n      internals.loseContext?.();\n      canvas.remove();\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [fragment, vertex, dpr, dprCap]);\n\n  // Live-sync caller uniforms every commit; re-render if the loop is idle so\n  // static frames reflect prop changes (paused, reduced motion).\n  React.useEffect(() => {\n    const it = internalsRef.current;\n    if (!it || !programLinked(it.program)) return;\n    syncUniforms(it.program, uniforms);\n    if (paused || reducedMotion) {\n      if (reducedMotion) it.uTime.value = reducedMotionTime;\n      it.renderer.render({ scene: it.mesh });\n    }\n  });\n\n  // Render the frozen frame whenever the static conditions change.\n  React.useEffect(() => {\n    const it = internalsRef.current;\n    if (!it || !ready || !programLinked(it.program)) return;\n    if (paused || reducedMotion) {\n      if (reducedMotion) {\n        it.uTime.value = reducedMotionTime;\n        it.uPointer.value[0] = 0.5;\n        it.uPointer.value[1] = 0.5;\n      }\n      it.renderer.render({ scene: it.mesh });\n    }\n  }, [paused, reducedMotion, reducedMotionTime, ready]);\n\n  return (\n    <div\n      ref={containerRef}\n      className={cn(\"relative block h-full w-full overflow-hidden\", className)}\n      {...rest}\n    >\n      {failed && (\n        <div\n          aria-hidden\n          className=\"absolute inset-0\"\n          style={{\n            background:\n              \"radial-gradient(120% 90% at 50% 100%, rgba(255,107,53,0.16), rgba(193,18,31,0.08) 45%, transparent 75%)\",\n          }}\n        />\n      )}\n    </div>\n  );\n}\n"
    }
  ]
}