{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dither-field",
  "title": "Dither Field",
  "description": "An animated WebGL2 background of Bayer-dithered square dots. The dots follow a light pattern, an image or a video, take one color or a three-stop ramp, and brighten under the pointer.",
  "files": [
    {
      "path": "registry/magicui/dither-field.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useRef, useState, type CSSProperties } from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/**\n * Each pattern is the GLSL body of `float pattern(vec2 p, vec2 size, float t)`.\n * `p` is the position in CSS pixels from the bottom-left corner, `size` is\n * the field size in CSS pixels and `t` is the time in seconds. Return the\n * light level, where 0 is dark and 1 is full light. Add an entry here to add\n * a pattern; the `pattern` prop type follows.\n */\nconst PATTERNS = {\n  // A low sun on the bottom edge, with haze bands that drift up through it.\n  horizon: `\n    float r = max(min(size.x * 0.42, size.y * 0.8), 90.0);\n    vec2 d = (p - vec2(0.5 * size.x, -0.15 * r)) / vec2(1.25 * r, r);\n    float wobble = fbm(vec2(atan(d.y, d.x) * 1.8, t * 0.04)) - 0.5;\n    float rim = length(d) * (1.0 + 0.14 * wobble);\n    float sun = exp(-3.4 * max(rim - 0.42, 0.0)) * 0.78;\n    float h = p.y / r;\n    float drift = (fbm(vec2(p.x / 320.0, t * 0.03)) - 0.5) * 2.4;\n    float band = 0.5 + 0.5 * sin(log(1.0 + 7.0 * h) * 8.0 - t * 0.45 + drift);\n    float haze = smoothstep(0.25, 0.95, band) * exp(-1.9 * h) * 0.5;\n    float ground = exp(-p.y / 22.0) * 0.32;\n    float sides = smoothstep(0.0, 0.2, p.x / size.x) * smoothstep(1.0, 0.8, p.x / size.x);\n    return (sun * (1.0 + haze) + haze * 0.35 + ground) * sides * (1.0 + 0.04 * sin(t * 0.4));\n  `,\n  // A soft orb in the middle that breathes and wanders a little.\n  glow: `\n    vec2 c = 0.5 * size + vec2(sin(t * 0.21), cos(t * 0.17)) * 0.05 * size;\n    float r = 0.5 * min(size.x, size.y);\n    float d = length(p - c) / r;\n    float n = fbm(p / 170.0 + vec2(t * 0.05, -t * 0.04));\n    return exp(-1.7 * d * d) * (0.75 + 0.45 * n) + 0.12 * n * exp(-d);\n  `,\n  // Curtains of light that hang from the top and sway.\n  aurora: `\n    float sway = fbm(vec2(p.x / 600.0, t * 0.05));\n    float d = p.y / size.y - (0.45 + 0.35 * (sway - 0.5));\n    float curtain = d > 0.0 ? exp(-d * 4.0) : exp(d * 22.0);\n    float rays = fbm(vec2(p.x / 70.0 + sway * 6.0, p.y / 900.0 - t * 0.08));\n    float shimmer = fbm(vec2(p.x / 40.0 - t * 0.05, 3.0));\n    return curtain * (0.25 + 0.75 * smoothstep(0.3, 0.75, rays)) * (0.6 + 0.5 * shimmer);\n  `,\n  // Slow diagonal swells, like dunes or contour lines.\n  waves: `\n    vec2 q = p / 32.0;\n    float warp = fbm(q * 0.09 + vec2(t * 0.035, t * 0.02));\n    float s = 0.5 + 0.5 * sin(q.x * 0.9 + q.y * 0.6 + warp * 5.0 - t * 0.4);\n    float crest = s * s * s * s;\n    return crest * (0.5 + 0.5 * p.y / size.y) + 0.12 * warp;\n  `,\n  // Drifting clouds that fill the whole area evenly.\n  clouds: `\n    float n = fbm(p / 210.0 + vec2(t * 0.03, t * 0.012));\n    float m = fbm(p / 80.0 - vec2(t * 0.02, 0.0));\n    return smoothstep(0.28, 0.78, n) * (0.7 + 0.5 * m);\n  `,\n} as const\n\nexport type DitherPattern = keyof typeof PATTERNS\n\n/** One colour, or a `[low, mid, high]` ramp from dim dots to bright dots. */\nexport type DitherColor = string | readonly [string, string, string]\n\nexport interface DitherFieldProps extends Omit<\n  React.ComponentPropsWithoutRef<\"div\">,\n  \"color\"\n> {\n  /**\n   * The procedural light field that the dots follow.\n   * @default \"horizon\"\n   */\n  pattern?: DitherPattern\n  /**\n   * An image or video URL to dither instead of the pattern. Bright areas get\n   * more dots. URLs ending in .mp4, .webm, .ogv, .mov or .m4v (or\n   * `data:video/` URLs) play as muted, looping video. Remote files must allow\n   * CORS.\n   */\n  src?: string\n  /**\n   * Any CSS colour, including `var(--primary)` and `currentColor`, or a\n   * `[low, mid, high]` ramp. One colour fades from 35% to full opacity.\n   * @default \"currentColor\"\n   */\n  color?: DitherColor\n  /**\n   * The size of one dot in CSS pixels. Dots snap to whole device pixels.\n   * @default 1\n   */\n  dotSize?: number\n  /**\n   * Brightness of the field. 0 shows only the pointer glow; above 1 fills more.\n   * @default 0.75\n   */\n  intensity?: number\n  /**\n   * Animation speed multiplier. `0` holds a still frame.\n   * @default 1\n   */\n  speed?: number\n  /**\n   * Frame rate cap. The dots read as a still texture, so 30 is plenty.\n   * @default 30\n   */\n  fps?: number\n  /**\n   * Brighten the field around the mouse, the pen or a held finger.\n   * @default true\n   */\n  interactive?: boolean\n}\n\ntype Rgba = [number, number, number, number]\n\ntype Media = HTMLImageElement | HTMLVideoElement\n\ntype Renderer = {\n  /** Sizes the dot grid (canvas pixels) and the field (CSS pixels). */\n  resize: (dotsX: number, dotsY: number, cssX: number, cssY: number) => void\n  setRamp: (ramp: Rgba[]) => void\n  setIntensity: (intensity: number) => void\n  setMedia: (media: Media | null) => void\n  draw: (time: number, pointer: [number, number, number]) => void\n  dispose: () => void\n}\n\nconst VERTEX = `#version 300 es\nvoid main() {\n  vec2 corner = vec2(gl_VertexID == 1 ? 3.0 : -1.0, gl_VertexID == 2 ? 3.0 : -1.0);\n  gl_Position = vec4(corner, 0.0, 1.0);\n}`\n\nconst FIELD_HEADER = `#version 300 es\nprecision highp float;\nuniform vec2 uSize;\nuniform vec2 uView;\nuniform float uTime;\nout vec4 outColor;\n\nfloat hash(vec2 p) {\n  return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);\n}\n\nfloat noise(vec2 p) {\n  vec2 i = floor(p);\n  vec2 f = fract(p);\n  f = f * f * (3.0 - 2.0 * f);\n  float a = hash(i);\n  float b = hash(i + vec2(1.0, 0.0));\n  float c = hash(i + vec2(0.0, 1.0));\n  float d = hash(i + vec2(1.0, 1.0));\n  return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);\n}\n\nfloat fbm(vec2 p) {\n  float sum = 0.0;\n  float amp = 0.5;\n  mat2 turn = mat2(0.8, 0.6, -0.6, 0.8);\n  for (int i = 0; i < 5; i++) {\n    sum += amp * noise(p);\n    p = turn * p * 2.0 + 11.7;\n    amp *= 0.5;\n  }\n  return sum;\n}\n`\n\nconst FIELD_MAIN = `\nvoid main() {\n  vec2 p = gl_FragCoord.xy / uView * uSize;\n  float v = pattern(p, uSize, uTime);\n  outColor = vec4(clamp(v, 0.0, 1.0), 0.0, 0.0, 1.0);\n}`\n\n// Image and video: luminance under a cover fit, so bright areas get more dots.\nconst MEDIA_PATTERN = `\nuniform sampler2D uMedia;\nuniform vec2 uMediaSize;\n\nfloat pattern(vec2 p, vec2 size, float t) {\n  float scale = max(size.x / uMediaSize.x, size.y / uMediaSize.y);\n  vec2 uv = (p - 0.5 * size) / (uMediaSize * scale) + 0.5;\n  vec4 c = texture(uMedia, vec2(uv.x, 1.0 - uv.y));\n  return dot(c.rgb, vec3(0.2126, 0.7152, 0.0722));\n}\n`\n\nconst DITHER = `#version 300 es\nprecision highp float;\nuniform sampler2D uField;\nuniform vec2 uView;\nuniform vec4 uLow;\nuniform vec4 uMid;\nuniform vec4 uHigh;\nuniform float uIntensity;\nuniform vec3 uPointer;\nuniform float uDot;\nout vec4 outColor;\n\n// 8x8 ordered-dither threshold, built from the 2x2 matrix [0 2; 3 1]. The\n// lowest bit of the cell position carries the largest weight.\nfloat threshold(vec2 cell) {\n  vec2 c = floor(cell);\n  float sum = 0.0;\n  float weight = 16.0;\n  for (int i = 0; i < 3; i++) {\n    vec2 b = mod(c, 2.0);\n    sum += weight * mod(2.0 * b.x + 3.0 * b.y, 4.0);\n    c = floor(c * 0.5);\n    weight *= 0.25;\n  }\n  return (sum + 0.5) / 64.0;\n}\n\nvoid main() {\n  vec2 uv = gl_FragCoord.xy / uView;\n  float base = texture(uField, uv).r;\n  float halo = textureLod(uField, uv, 1.0).r * 0.45\n    + textureLod(uField, uv, 2.2).r * 0.35\n    + textureLod(uField, uv, 3.4).r * 0.2;\n  float v = base + 0.55 * halo;\n  float over = max(v - 0.85, 0.0);\n  v = min(v, 0.85) + over / (1.0 + 4.0 * over);\n  v = clamp(v * uIntensity, 0.0, 1.0);\n  // The pointer glow comes after intensity, so it shows on quiet fields too.\n  vec2 away = gl_FragCoord.xy * uDot - uPointer.xy;\n  v = min(v + uPointer.z * exp(-dot(away, away) / 28000.0) * (0.35 + 0.5 * v), 1.0);\n  // Drop the faintest light, so empty areas stay empty instead of showing\n  // a sparse, even grid.\n  v = max(v - 0.02, 0.0) / 0.98;\n  float on = step(threshold(gl_FragCoord.xy), v);\n  vec4 c = v < 0.5 ? mix(uLow, uMid, v * 2.0) : mix(uMid, uHigh, v * 2.0 - 1.0);\n  outColor = vec4(c.rgb * c.a, c.a) * on;\n}`\n\n/** The light field renders at this fraction of the CSS size, then blooms. */\nconst FIELD_SCALE = 0.6\nconst POINTER_EASE = 6\nconst VIDEO_FILE = /^data:video\\/|\\.(mp4|webm|ogv|mov|m4v)(\\?|#|$)/i\n\nfunction compile(gl: WebGL2RenderingContext, fragment: string) {\n  const program = gl.createProgram()\n  for (const [type, source] of [\n    [gl.VERTEX_SHADER, VERTEX],\n    [gl.FRAGMENT_SHADER, fragment],\n  ] as const) {\n    const shader = gl.createShader(type)\n    if (!shader) return null\n    gl.shaderSource(shader, source)\n    gl.compileShader(shader)\n    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n      console.warn(\"DitherField: shader error\", gl.getShaderInfoLog(shader))\n    }\n    gl.attachShader(program, shader)\n    gl.deleteShader(shader)\n  }\n  gl.linkProgram(program)\n  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n    gl.deleteProgram(program)\n    return null\n  }\n  return program\n}\n\nfunction createTexture(gl: WebGL2RenderingContext, mipmaps: boolean) {\n  const texture = gl.createTexture()\n  gl.bindTexture(gl.TEXTURE_2D, texture)\n  const min = mipmaps ? gl.LINEAR_MIPMAP_LINEAR : gl.LINEAR\n  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, min)\n  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)\n  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)\n  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)\n  return texture\n}\n\n/**\n * Two passes: the light field renders small into a mipmapped texture, then\n * the dither pass reads it with a bloom from the blurred mip levels and\n * thresholds it into dots, one canvas pixel per dot. Returns null when the\n * shaders do not compile, so the caller can fall back.\n */\nfunction createRenderer(\n  gl: WebGL2RenderingContext,\n  patternSource: string\n): Renderer | null {\n  const field = compile(gl, `${FIELD_HEADER}${patternSource}${FIELD_MAIN}`)\n  const dither = compile(gl, DITHER)\n  if (!field || !dither) {\n    gl.deleteProgram(field)\n    gl.deleteProgram(dither)\n    return null\n  }\n  const at = (program: WebGLProgram, name: string) =>\n    gl.getUniformLocation(program, name)\n\n  const fieldTexture = createTexture(gl, true)\n  const mediaTexture = createTexture(gl, false)\n  const framebuffer = gl.createFramebuffer()\n  let media: Media | null = null\n  // Images upload once; video uploads a new frame on every draw.\n  let mediaStale = false\n  let view = [1, 1]\n  let fieldView = [1, 1]\n  let css = [1, 1]\n\n  gl.useProgram(dither)\n  gl.uniform1i(at(dither, \"uField\"), 0)\n  gl.useProgram(field)\n  gl.uniform1i(at(field, \"uMedia\"), 1)\n\n  return {\n    resize(dotsX, dotsY, cssX, cssY) {\n      view = [dotsX, dotsY]\n      css = [cssX, cssY]\n      fieldView = [\n        Math.max(1, Math.round(cssX * FIELD_SCALE)),\n        Math.max(1, Math.round(cssY * FIELD_SCALE)),\n      ]\n      gl.activeTexture(gl.TEXTURE0)\n      gl.bindTexture(gl.TEXTURE_2D, fieldTexture)\n      gl.texImage2D(\n        gl.TEXTURE_2D,\n        0,\n        gl.RGBA,\n        fieldView[0],\n        fieldView[1],\n        0,\n        gl.RGBA,\n        gl.UNSIGNED_BYTE,\n        null\n      )\n      gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer)\n      gl.framebufferTexture2D(\n        gl.FRAMEBUFFER,\n        gl.COLOR_ATTACHMENT0,\n        gl.TEXTURE_2D,\n        fieldTexture,\n        0\n      )\n      gl.bindFramebuffer(gl.FRAMEBUFFER, null)\n    },\n    setRamp([low, mid, high]) {\n      gl.useProgram(dither)\n      gl.uniform4f(at(dither, \"uLow\"), ...low)\n      gl.uniform4f(at(dither, \"uMid\"), ...mid)\n      gl.uniform4f(at(dither, \"uHigh\"), ...high)\n    },\n    setIntensity(intensity) {\n      gl.useProgram(dither)\n      gl.uniform1f(at(dither, \"uIntensity\"), intensity)\n    },\n    setMedia(next) {\n      media = next\n      mediaStale = true\n    },\n    draw(time, pointer) {\n      gl.useProgram(field)\n      if (media) {\n        const source = media\n        const isVideo = source instanceof HTMLVideoElement\n        const width = isVideo ? source.videoWidth : source.naturalWidth\n        const height = isVideo ? source.videoHeight : source.naturalHeight\n        if (!width || !height) return\n        if (mediaStale || isVideo) {\n          gl.activeTexture(gl.TEXTURE1)\n          gl.bindTexture(gl.TEXTURE_2D, mediaTexture)\n          gl.texImage2D(\n            gl.TEXTURE_2D,\n            0,\n            gl.RGBA,\n            gl.RGBA,\n            gl.UNSIGNED_BYTE,\n            source\n          )\n          gl.uniform2f(at(field, \"uMediaSize\"), width, height)\n          mediaStale = false\n        }\n      }\n      gl.uniform2f(at(field, \"uSize\"), css[0], css[1])\n      gl.uniform2f(at(field, \"uView\"), fieldView[0], fieldView[1])\n      gl.uniform1f(at(field, \"uTime\"), time)\n      gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer)\n      gl.viewport(0, 0, fieldView[0], fieldView[1])\n      gl.drawArrays(gl.TRIANGLES, 0, 3)\n      gl.bindFramebuffer(gl.FRAMEBUFFER, null)\n      gl.activeTexture(gl.TEXTURE0)\n      gl.bindTexture(gl.TEXTURE_2D, fieldTexture)\n      gl.generateMipmap(gl.TEXTURE_2D)\n\n      gl.useProgram(dither)\n      gl.uniform2f(at(dither, \"uView\"), view[0], view[1])\n      gl.uniform1f(at(dither, \"uDot\"), css[0] / view[0])\n      gl.uniform3f(at(dither, \"uPointer\"), ...pointer)\n      gl.viewport(0, 0, view[0], view[1])\n      gl.drawArrays(gl.TRIANGLES, 0, 3)\n    },\n    dispose() {\n      gl.deleteProgram(field)\n      gl.deleteProgram(dither)\n      gl.deleteTexture(fieldTexture)\n      gl.deleteTexture(mediaTexture)\n      gl.deleteFramebuffer(framebuffer)\n    },\n  }\n}\n\nconst toRamp = (color: DitherColor) =>\n  typeof color === \"string\" ? [color, color, color] : [...color]\n\n/**\n * Resolves CSS colours (`var(--x)`, `currentColor`, `oklch()`) in the\n * context of `host`, then reads them back as straight RGBA through a\n * 1x1 canvas. A single colour fades from 35% to full opacity.\n */\nfunction resolveRamp(host: HTMLElement, color: DitherColor): Rgba[] {\n  const probe = document.createElement(\"span\")\n  probe.style.display = \"none\"\n  host.appendChild(probe)\n  const context = document\n    .createElement(\"canvas\")\n    .getContext(\"2d\", { willReadFrequently: true })\n  const fades = typeof color === \"string\" ? [0.35, 0.75, 1] : [1, 1, 1]\n  const ramp = toRamp(color).map((value, index): Rgba => {\n    probe.style.color = \"\"\n    probe.style.color = value\n    if (!context) return [0.5, 0.5, 0.5, fades[index]]\n    context.clearRect(0, 0, 1, 1)\n    context.fillStyle = \"#808080\"\n    context.fillStyle = getComputedStyle(probe).color\n    context.fillRect(0, 0, 1, 1)\n    const [r, g, b, a] = context.getImageData(0, 0, 1, 1).data\n    return [r / 255, g / 255, b / 255, (a / 255) * fades[index]]\n  })\n  probe.remove()\n  return ramp\n}\n\nfunction loadMedia(\n  src: string,\n  onReady: (media: Media) => void,\n  onError: () => void\n) {\n  if (VIDEO_FILE.test(src)) {\n    const video = document.createElement(\"video\")\n    video.crossOrigin = \"anonymous\"\n    video.muted = true\n    video.loop = true\n    video.playsInline = true\n    video.preload = \"auto\"\n    video.addEventListener(\"loadeddata\", () => onReady(video), { once: true })\n    video.addEventListener(\"error\", onError, { once: true })\n    video.src = src\n    return video\n  }\n  const image = new Image()\n  image.crossOrigin = \"anonymous\"\n  image.decoding = \"async\"\n  image.addEventListener(\"load\", () => onReady(image), { once: true })\n  image.addEventListener(\"error\", onError, { once: true })\n  image.src = src\n  return image\n}\n\nconst ease = (from: number, to: number, rate: number, dt: number) =>\n  from + (to - from) * (1 - Math.exp(-rate * dt))\n\n/**\n * An animated ordered-dither background. A soft light field, or an image or\n * video, is thresholded against an 8x8 Bayer matrix into crisp square dots\n * coloured from a three-stop ramp. Children render above the dots.\n */\nexport function DitherField({\n  pattern = \"horizon\",\n  src,\n  color = \"currentColor\",\n  dotSize = 1,\n  intensity = 0.75,\n  speed = 1,\n  fps = 30,\n  interactive = true,\n  className,\n  children,\n  ...props\n}: DitherFieldProps) {\n  const rootRef = useRef<HTMLDivElement>(null)\n  const layerRef = useRef<HTMLDivElement>(null)\n  // The GL context only exists while the field is near the viewport, so a\n  // page with many fields stays under the browser's cap of live contexts.\n  const [near, setNear] = useState(false)\n  const [fallback, setFallback] = useState(false)\n  // Bumped when the browser restores a lost WebGL context.\n  const [contextKey, setContextKey] = useState(0)\n  // Read every frame, so changing them never rebuilds the GL state.\n  const live = useRef({ color, intensity, speed, fps, interactive })\n  live.current = { color, intensity, speed, fps, interactive }\n  const repaint = useRef(() => {})\n  // Kept across rebuilds, so a field that scrolls back into view resumes.\n  const time = useRef(10)\n  const shown = useRef(false)\n  const colorKey = toRamp(color).join(\"|\")\n\n  useEffect(() => {\n    const root = rootRef.current\n    if (!root) return\n    const observer = new IntersectionObserver(\n      ([entry]) => setNear(entry.isIntersecting),\n      { rootMargin: \"100px\" }\n    )\n    observer.observe(root)\n    return () => observer.disconnect()\n  }, [])\n\n  useEffect(() => {\n    const root = rootRef.current\n    const layer = layerRef.current\n    if (!near || !root || !layer) return\n\n    const canvas = document.createElement(\"canvas\")\n    canvas.className =\n      \"absolute top-0 left-0 opacity-0 transition-opacity duration-700 [image-rendering:pixelated] data-[ready]:opacity-100 motion-reduce:transition-none\"\n    const gl = canvas.getContext(\"webgl2\", {\n      alpha: true,\n      premultipliedAlpha: true,\n      antialias: false,\n      depth: false,\n      stencil: false,\n      powerPreference: \"low-power\",\n    })\n    const renderer = gl\n      ? createRenderer(\n          gl,\n          src\n            ? MEDIA_PATTERN\n            : `float pattern(vec2 p, vec2 size, float t) {${PATTERNS[pattern]}}`\n        )\n      : null\n    if (!gl || !renderer) {\n      gl?.getExtension(\"WEBGL_lose_context\")?.loseContext()\n      setFallback(true)\n      return\n    }\n    setFallback(false)\n    // A field that comes back into view skips the fade-in.\n    if (shown.current) canvas.dataset.ready = \"\"\n    layer.appendChild(canvas)\n\n    const reducedMotion = window.matchMedia(\"(prefers-reduced-motion: reduce)\")\n    const pointer = { x: 0, y: 0, clientX: 0, clientY: 0, on: false, hover: 0 }\n    let previous = 0\n    let frame = 0\n    let cssHeight = 0\n    let ready = !src\n    let lost = false\n    let disposed = false\n\n    const draw = () => {\n      if (!ready || !cssHeight || lost) return\n      renderer.draw(time.current, [pointer.x, pointer.y, pointer.hover])\n      canvas.dataset.ready = \"\"\n      shown.current = true\n    }\n\n    const paint = () => {\n      renderer.setRamp(resolveRamp(root, live.current.color))\n      renderer.setIntensity(live.current.intensity)\n      draw()\n    }\n    repaint.current = paint\n    // Theme changes can arrive in bursts; resolve the colours once a frame.\n    let paintFrame = 0\n    const schedulePaint = () => {\n      cancelAnimationFrame(paintFrame)\n      paintFrame = requestAnimationFrame(paint)\n    }\n\n    // One dot is a whole number of device pixels, so dots stay crisp\n    // squares on any screen. The canvas may overhang the root by under a\n    // dot; the root clips it. Layout size, so transforms on a parent (an\n    // entrance animation, say) do not shrink the grid.\n    const resize = () => {\n      const width = root.clientWidth\n      const height = root.clientHeight\n      if (!width || !height) return\n      const dpr = window.devicePixelRatio || 1\n      const devicePerDot = Math.max(1, Math.round(dotSize * dpr))\n      const cssPerDot = devicePerDot / dpr\n      const dotsX = Math.ceil((width * dpr) / devicePerDot)\n      const dotsY = Math.ceil((height * dpr) / devicePerDot)\n      cssHeight = dotsY * cssPerDot\n      canvas.width = dotsX\n      canvas.height = dotsY\n      canvas.style.width = `${dotsX * cssPerDot}px`\n      canvas.style.height = `${cssHeight}px`\n      renderer.resize(dotsX, dotsY, dotsX * cssPerDot, cssHeight)\n      draw()\n    }\n\n    // Animate only while the tab is visible and motion is allowed.\n    // Otherwise hold the last frame.\n    const sync = () => {\n      const running =\n        ready && !lost && !document.hidden && !reducedMotion.matches\n      if (running && !frame) {\n        previous = 0\n        frame = requestAnimationFrame(tick)\n        video?.play().catch(() => {})\n      } else if (!running && frame) {\n        cancelAnimationFrame(frame)\n        frame = 0\n        video?.pause()\n      }\n    }\n\n    const tick = (now: number) => {\n      frame = requestAnimationFrame(tick)\n      const { speed, fps, interactive } = live.current\n      if (previous && now - previous < 1000 / Math.max(fps, 1) - 2) return\n      // Real time since the last drawn frame, so a low fps keeps the same\n      // speed. The clock restarts on resume, and a stalled main thread\n      // cannot jump the field by more than 2 s.\n      const dt = previous ? Math.min((now - previous) / 1000, 2) : 0\n      previous = now\n      time.current += dt * speed\n      const box = root.getBoundingClientRect()\n      const left = box.left + root.clientLeft\n      const top = box.top + root.clientTop\n      const inside =\n        interactive &&\n        pointer.on &&\n        pointer.clientX >= box.left &&\n        pointer.clientX <= box.right &&\n        pointer.clientY >= box.top &&\n        pointer.clientY <= box.bottom\n      pointer.x = ease(pointer.x, pointer.clientX - left, POINTER_EASE, dt)\n      pointer.y = ease(\n        pointer.y,\n        top + cssHeight - pointer.clientY,\n        POINTER_EASE,\n        dt\n      )\n      pointer.hover = ease(pointer.hover, inside ? 1 : 0, 4, dt)\n      draw()\n    }\n\n    const media = src\n      ? loadMedia(\n          src,\n          (source) => {\n            if (disposed) return\n            renderer.setMedia(source)\n            ready = true\n            draw()\n            sync()\n          },\n          () => {\n            if (!disposed) setFallback(true)\n          }\n        )\n      : null\n    const video = media instanceof HTMLVideoElement ? media : null\n\n    const onPointer = (event: PointerEvent) => {\n      // A finger only counts while it is down. Hover events from a lifted\n      // finger would leave the glow stuck where it last tapped.\n      if (event.pointerType === \"touch\" && event.buttons === 0) return\n      if (!pointer.on) {\n        const box = root.getBoundingClientRect()\n        pointer.x = event.clientX - box.left - root.clientLeft\n        pointer.y = box.top + root.clientTop + cssHeight - event.clientY\n      }\n      pointer.clientX = event.clientX\n      pointer.clientY = event.clientY\n      pointer.on = true\n    }\n    const onLeave = () => {\n      pointer.on = false\n    }\n    // Lifting the finger, or the browser taking over the touch to scroll,\n    // fades the glow out. The listeners are passive, so scrolling is never\n    // blocked.\n    const onTouchEnd = (event: PointerEvent) => {\n      if (event.pointerType === \"touch\") pointer.on = false\n    }\n\n    // A move to a screen with another pixel ratio changes the dot size\n    // without resizing the element.\n    let ratioQuery: MediaQueryList | null = null\n    const watchRatio = () => {\n      ratioQuery?.removeEventListener(\"change\", onRatio)\n      ratioQuery = window.matchMedia(\n        `(resolution: ${window.devicePixelRatio}dppx)`\n      )\n      ratioQuery.addEventListener(\"change\", onRatio)\n    }\n    const onRatio = () => {\n      resize()\n      watchRatio()\n    }\n\n    // A lost context shows the CSS dots until the browser gives it back.\n    const onLost = (event: Event) => {\n      event.preventDefault()\n      lost = true\n      sync()\n      setFallback(true)\n    }\n    const onRestored = () => setContextKey((key) => key + 1)\n\n    const resizeObserver = new ResizeObserver(resize)\n    // Theme switches usually toggle a class or attribute on <html>.\n    const themeObserver = new MutationObserver(schedulePaint)\n    const darkQuery = window.matchMedia(\"(prefers-color-scheme: dark)\")\n\n    resize()\n    paint()\n    sync()\n    watchRatio()\n    resizeObserver.observe(root)\n    themeObserver.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\", \"data-theme\"],\n    })\n    // The root's own class or style can change `currentColor` or a CSS\n    // variable in the ramp, for example text-red-500 to text-blue-500.\n    themeObserver.observe(root, {\n      attributes: true,\n      attributeFilter: [\"class\", \"style\"],\n    })\n    darkQuery.addEventListener(\"change\", schedulePaint)\n    reducedMotion.addEventListener(\"change\", sync)\n    document.addEventListener(\"visibilitychange\", sync)\n    window.addEventListener(\"pointerdown\", onPointer, { passive: true })\n    window.addEventListener(\"pointermove\", onPointer, { passive: true })\n    window.addEventListener(\"pointerup\", onTouchEnd, { passive: true })\n    window.addEventListener(\"pointercancel\", onTouchEnd, { passive: true })\n    document.documentElement.addEventListener(\"pointerleave\", onLeave)\n    canvas.addEventListener(\"webglcontextlost\", onLost)\n    canvas.addEventListener(\"webglcontextrestored\", onRestored)\n\n    return () => {\n      disposed = true\n      cancelAnimationFrame(frame)\n      cancelAnimationFrame(paintFrame)\n      repaint.current = () => {}\n      resizeObserver.disconnect()\n      themeObserver.disconnect()\n      ratioQuery?.removeEventListener(\"change\", onRatio)\n      darkQuery.removeEventListener(\"change\", schedulePaint)\n      reducedMotion.removeEventListener(\"change\", sync)\n      document.removeEventListener(\"visibilitychange\", sync)\n      window.removeEventListener(\"pointerdown\", onPointer)\n      window.removeEventListener(\"pointermove\", onPointer)\n      window.removeEventListener(\"pointerup\", onTouchEnd)\n      window.removeEventListener(\"pointercancel\", onTouchEnd)\n      document.documentElement.removeEventListener(\"pointerleave\", onLeave)\n      canvas.removeEventListener(\"webglcontextlost\", onLost)\n      canvas.removeEventListener(\"webglcontextrestored\", onRestored)\n      if (video) {\n        video.pause()\n        video.removeAttribute(\"src\")\n        video.load()\n      } else {\n        media?.removeAttribute(\"src\")\n      }\n      renderer.dispose()\n      gl.getExtension(\"WEBGL_lose_context\")?.loseContext()\n      canvas.remove()\n    }\n  }, [near, pattern, src, dotSize, contextKey])\n\n  useEffect(() => {\n    repaint.current()\n  }, [colorKey, intensity])\n\n  const [, mid] = toRamp(color)\n  const fallbackStyle: CSSProperties = {\n    backgroundImage: `radial-gradient(${mid} 0.6px, transparent 0.9px)`,\n    backgroundSize: `${3 * dotSize}px ${3 * dotSize}px`,\n    maskImage: \"radial-gradient(ellipse at 50% 70%, black, transparent 75%)\",\n    WebkitMaskImage:\n      \"radial-gradient(ellipse at 50% 70%, black, transparent 75%)\",\n    opacity: Math.min(1, intensity),\n  }\n\n  return (\n    <div\n      ref={rootRef}\n      className={cn(\"relative isolate overflow-hidden\", className)}\n      {...props}\n    >\n      <div\n        ref={layerRef}\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute inset-0 -z-10\"\n        style={fallback ? fallbackStyle : undefined}\n      />\n      {children}\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}