{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"contour-map","type":"registry:ui","title":"Contour Map","description":"Animated topographic contour lines over slowly shifting terrain, with a hill that rises under the cursor. WebGL.","dependencies":[],"registryDependencies":[],"files":[{"path":"registry/ui/contour-map.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\nexport interface ContourMapProps {\n  /** Color of the low contour lines. */\n  color?: string;\n  /** Color of the peaks; lines blend toward it with height. */\n  peakColor?: string;\n  /** Number of contour levels; higher means denser lines. */\n  levels?: number;\n  /** Line width in px. */\n  lineWidth?: number;\n  /** Drift speed multiplier; 0 freezes the terrain. */\n  speed?: number;\n  /** Terrain zoom; higher means larger hills. */\n  scale?: number;\n  /** Height of the hill that rises under the cursor, 0 to disable. */\n  cursorHill?: number;\n  className?: string;\n}\n\nconst VERT = \"attribute vec2 p;void main(){gl_Position=vec4(p,0.,1.);}\";\n\nconst FRAG = `\n#extension GL_OES_standard_derivatives : enable\nprecision highp float;\nuniform vec2 uRes;\nuniform float uTime;\nuniform vec2 uMouse;\nuniform float uHill;\nuniform vec3 uColor;\nuniform vec3 uPeak;\nuniform float uLevels;\nuniform float uWidth;\nuniform float uScale;\n\nfloat hash(vec2 p) {\n  p = fract(p * vec2(233.34, 851.73));\n  p += dot(p, p + 23.45);\n  return fract(p.x * p.y);\n}\nfloat noise(vec2 p) {\n  vec2 i = floor(p), f = fract(p);\n  vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);\n  return mix(mix(hash(i), hash(i + vec2(1, 0)), u.x), mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), u.x), u.y);\n}\nfloat fbm(vec2 p) {\n  float v = 0.0, a = 0.5;\n  mat2 m = mat2(1.6, 1.2, -1.2, 1.6);\n  for (int i = 0; i < 3; i++) { v += a * noise(p); p = m * p; a *= 0.5; }\n  return v;\n}\n\nvoid main() {\n  vec2 uv = gl_FragCoord.xy / uRes;\n  vec2 p = (gl_FragCoord.xy - 0.5 * uRes) / uRes.y * 2.2 / uScale;\n  vec2 m = (uMouse - 0.5) * uRes / uRes.y * 2.2 / uScale;\n  float t = uTime * 0.035;\n\n  // Slowly warped terrain, plus a soft hill that follows the cursor.\n  vec2 w = vec2(fbm(p * 0.35 + vec2(t, -t * 0.7)), fbm(p * 0.35 + vec2(4.1 - t * 0.8, 1.7 + t)));\n  float hgt = fbm(p * 0.55 + w * 0.8 + vec2(t * 0.5, 0.0));\n  vec2 dm = p - m;\n  hgt += uHill * 0.28 * exp(-dot(dm, dm) * 2.2 * uScale * uScale);\n\n  float v = hgt * uLevels;\n  float fw = max(fwidth(v), 1e-4);\n  float e = abs(fract(v + 0.5) - 0.5) / fw;\n  float idx = floor(v + 0.5);\n  float major = 1.0 - step(0.5, mod(idx, 5.0));\n  float lw = uWidth * (1.0 + major * 0.8);\n  float line = 1.0 - smoothstep(lw * 0.5 - 0.5, lw * 0.5 + 0.75, e);\n\n  float k = smoothstep(0.3, 0.68, hgt);\n  vec3 col = mix(uColor, uPeak, k);\n  float a = line * (0.5 + 0.4 * major + 0.2 * k);\n  // Faint elevation wash between the lines.\n  a += 0.05 * k * k;\n  vec2 q = uv - 0.5;\n  a *= clamp(1.25 - dot(q, q) * 2.2, 0.0, 1.0);\n  a = clamp(a, 0.0, 1.0);\n  gl_FragColor = vec4(col * a, a);\n}\n`;\n\nfunction rgb(hex: string) {\n  let h = (hex || \"#000\").replace(\"#\", \"\");\n  if (h.length === 3) h = h.replace(/./g, \"$&$&\");\n  const n = parseInt(h.padEnd(6, \"0\").slice(0, 6), 16) || 0;\n  return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];\n}\n\n/** Compiles a full-screen-triangle program and returns a cached uniform lookup, or null. */\nfunction fullscreen(gl: WebGLRenderingContext, frag: string) {\n  const prog = gl.createProgram();\n  if (!prog) return null;\n  for (const [type, src] of [\n    [gl.VERTEX_SHADER, VERT],\n    [gl.FRAGMENT_SHADER, frag],\n  ] as const) {\n    const sh = gl.createShader(type);\n    if (!sh) return null;\n    gl.shaderSource(sh, src);\n    gl.compileShader(sh);\n    if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) console.warn(gl.getShaderInfoLog(sh));\n    gl.attachShader(prog, sh);\n  }\n  gl.bindAttribLocation(prog, 0, \"p\");\n  gl.linkProgram(prog);\n  if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) return null;\n  gl.useProgram(prog);\n  gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());\n  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);\n  gl.enableVertexAttribArray(0);\n  gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);\n  const cache = new Map<string, WebGLUniformLocation | null>();\n  return (name: string) => {\n    if (!cache.has(name)) cache.set(name, gl.getUniformLocation(prog, name));\n    return cache.get(name) ?? null;\n  };\n}\n\nexport function ContourMap({\n  color = \"#38bdf8\",\n  peakColor = \"#c6ff3d\",\n  levels = 22,\n  lineWidth = 1,\n  speed = 1,\n  scale = 1,\n  cursorHill = 1,\n  className,\n}: ContourMapProps) {\n  const host = useRef<HTMLDivElement>(null);\n  const opts = useRef({ color, peakColor, levels, lineWidth, speed, scale, cursorHill });\n  const redraw = useRef<(() => void) | null>(null);\n\n  useEffect(() => {\n    opts.current = { color, peakColor, levels, lineWidth, speed, scale, cursorHill };\n    redraw.current?.();\n  }, [color, peakColor, levels, lineWidth, speed, scale, cursorHill]);\n\n  useEffect(() => {\n    const el = host.current!;\n    const canvas = document.createElement(\"canvas\");\n    canvas.style.cssText = \"position:absolute;inset:0;width:100%;height:100%;display:block\";\n    el.appendChild(canvas);\n    const gl = canvas.getContext(\"webgl\", { antialias: false, powerPreference: \"low-power\" });\n    const u = gl && gl.getExtension(\"OES_standard_derivatives\") && fullscreen(gl, FRAG);\n    if (!gl || !u) {\n      canvas.remove();\n      return;\n    }\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    // `a` eases the cursor hill in and out as the pointer enters and leaves.\n    const pointer = { x: 0.5, y: 0.5, tx: 0.5, ty: 0.5, a: 0, ta: 0 };\n    let raf = 0;\n    let visible = true;\n    let time = 30;\n    let last = 0;\n    const dpr = () => Math.min(window.devicePixelRatio || 1, 2);\n\n    const draw = () => {\n      const o = opts.current;\n      gl.uniform2f(u(\"uRes\"), canvas.width, canvas.height);\n      gl.uniform1f(u(\"uTime\"), time);\n      gl.uniform2f(u(\"uMouse\"), pointer.x, pointer.y);\n      gl.uniform1f(u(\"uHill\"), o.cursorHill * pointer.a);\n      gl.uniform3fv(u(\"uColor\"), rgb(o.color));\n      gl.uniform3fv(u(\"uPeak\"), rgb(o.peakColor));\n      gl.uniform1f(u(\"uLevels\"), Math.max(o.levels, 1));\n      gl.uniform1f(u(\"uWidth\"), Math.max(o.lineWidth, 0.25) * dpr());\n      gl.uniform1f(u(\"uScale\"), Math.max(o.scale, 0.05));\n      gl.drawArrays(gl.TRIANGLES, 0, 3);\n    };\n    const loop = (now: number) => {\n      time += (Math.min(now - last, 50) / 1000) * opts.current.speed;\n      last = now;\n      pointer.x += (pointer.tx - pointer.x) * 0.06;\n      pointer.y += (pointer.ty - pointer.y) * 0.06;\n      pointer.a += (pointer.ta - pointer.a) * 0.03;\n      draw();\n      raf = requestAnimationFrame(loop);\n    };\n    const play = () => {\n      if (raf || !visible || reduced) return;\n      last = performance.now();\n      raf = requestAnimationFrame(loop);\n    };\n    const resize = () => {\n      canvas.width = Math.max(1, Math.round(el.clientWidth * dpr()));\n      canvas.height = Math.max(1, Math.round(el.clientHeight * dpr()));\n      gl.viewport(0, 0, canvas.width, canvas.height);\n      draw();\n    };\n    const onMove = (e: PointerEvent) => {\n      const r = el.getBoundingClientRect();\n      pointer.tx = (e.clientX - r.left) / r.width;\n      pointer.ty = 1 - (e.clientY - r.top) / r.height;\n      if (pointer.ta === 0 && pointer.a < 0.01) {\n        pointer.x = pointer.tx;\n        pointer.y = pointer.ty;\n      }\n      pointer.ta = 1;\n    };\n    const onLeave = () => {\n      pointer.ta = 0;\n    };\n\n    const target = el.parentElement ?? el;\n    target.addEventListener(\"pointermove\", onMove, { passive: true });\n    target.addEventListener(\"pointerleave\", onLeave);\n    const ro = new ResizeObserver(resize);\n    ro.observe(el);\n    const io = new IntersectionObserver(([entry]) => {\n      visible = entry.isIntersecting;\n      if (visible) play();\n      else {\n        cancelAnimationFrame(raf);\n        raf = 0;\n      }\n    });\n    io.observe(el);\n    redraw.current = draw;\n\n    return () => {\n      cancelAnimationFrame(raf);\n      ro.disconnect();\n      io.disconnect();\n      target.removeEventListener(\"pointermove\", onMove);\n      target.removeEventListener(\"pointerleave\", onLeave);\n      redraw.current = null;\n      gl.getExtension(\"WEBGL_lose_context\")?.loseContext();\n      canvas.remove();\n    };\n  }, []);\n\n  return (\n    <div\n      ref={host}\n      aria-hidden\n      className={className}\n      style={{ position: \"absolute\", inset: 0, overflow: \"hidden\", pointerEvents: \"none\" }}\n    />\n  );\n}\n"}]}