{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dot-grid",
  "title": "Dot Grid",
  "description": "A grid of dots on springs: they scatter from the pointer, ripple out from a click and settle back. Raw WebGL 2, no dependencies.",
  "files": [
    {
      "path": "registry/mewo/ui/dot-grid.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useRef, type ComponentProps } from \"react\"\nimport { cn } from \"@/lib/utils\"\n\nconst VERTEX = `#version 300 es\nprecision highp float;\n\nlayout(location = 0) in vec2 aCorner;\nlayout(location = 1) in vec4 aMotion;\nlayout(location = 2) in vec2 aState;\n\nuniform vec2 uResolution;\nuniform float uSize;\nuniform float uSwell;\nuniform float uStretch;\nuniform float uGlow;\n\nout vec2 vLocal;\nout vec2 vPoint;\nout float vLength;\nout float vRadius;\nout float vEnergy;\n\nvoid main() {\n  float energy = aState.x;\n  float radius = 0.5 * uSize * aState.y * (1.0 + uSwell * energy);\n  float speed = length(aMotion.zw);\n  float angle = speed > 0.001 ? atan(aMotion.w, aMotion.z) : 0.0;\n  float axis = floor(angle / 1.5707963 + 0.5) * 1.5707963;\n  angle = axis + (angle - axis) * smoothstep(8.0, 70.0, speed);\n  vec2 dir = vec2(cos(angle), sin(angle));\n  float halfLength = min(uSize * 2.0, speed * uStretch * 0.02);\n  float pad = radius * (1.0 + uGlow * energy * 2.5) + 2.0;\n  vec2 local = aCorner * vec2(halfLength + pad, pad);\n  vec2 point = aMotion.xy + dir * local.x + vec2(-dir.y, dir.x) * local.y;\n  vLocal = local;\n  vPoint = point;\n  vLength = halfLength;\n  vRadius = radius;\n  vEnergy = energy;\n  vec2 clip = point / uResolution * 2.0 - 1.0;\n  gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);\n}\n`\n\nconst FRAGMENT = `#version 300 es\nprecision highp float;\n\nin vec2 vLocal;\nin vec2 vPoint;\nin float vLength;\nin float vRadius;\nin float vEnergy;\n\nuniform vec2 uResolution;\nuniform vec3 uBase;\nuniform vec3 uActive;\nuniform float uGlow;\nuniform float uOpacity;\nuniform float uFade;\nuniform int uShape;\n\nout vec4 outColor;\n\nfloat smootherstep01(float t) {\n  t = clamp(t, 0.0, 1.0);\n  return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);\n}\n\nvoid main() {\n  float d;\n  if (uShape == 1) {\n    float corner = vRadius * 0.3;\n    vec2 q = abs(vLocal) - vec2(vLength + vRadius, vRadius) + corner;\n    d = length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - corner;\n  } else {\n    d = length(vec2(max(abs(vLocal.x) - vLength, 0.0), vLocal.y)) - vRadius;\n  }\n  float aa = max(fwidth(d), 0.0001);\n  float body = clamp(0.5 - d / aa, 0.0, 1.0);\n  float halo = uGlow * vEnergy * 0.5 * exp(-max(d, 0.0) / max(vRadius * 1.3, 0.6));\n  float alpha = body + (1.0 - body) * halo;\n  vec3 color = mix(uBase, uActive, clamp(vEnergy * 1.2, 0.0, 1.0));\n  float fade = 1.0;\n  if (uFade > 0.0) {\n    vec2 edge = min(vPoint, uResolution - vPoint) / (uFade * 0.4 * min(uResolution.x, uResolution.y));\n    fade = smootherstep01(edge.x) * smootherstep01(edge.y);\n  }\n  alpha *= uOpacity * fade;\n  outColor = vec4(color * alpha, alpha);\n}\n`\n\ntype Shape = \"circle\" | \"square\"\n\nexport type DotGridShape = Shape\n\nexport interface DotGridProps extends ComponentProps<\"div\"> {\n  dotSize?: number\n  gap?: number\n  /** Colour of a resting dot. */\n  baseColor?: string\n  /** Colour a dot moves towards as it gets excited. */\n  activeColor?: string\n  /** How close the pointer has to be to push a dot, in pixels. */\n  proximity?: number\n  strength?: number\n  /** How springy the return is: 0 settles, 1 overshoots. */\n  bounce?: number\n  /** How much neighbouring dots pull on each other. */\n  tension?: number\n  returnDuration?: number\n  /** Reach of the ring a click sends out, in pixels. */\n  shockRadius?: number\n  shockStrength?: number\n  /** How much a moving dot grows. */\n  swell?: number\n  /** How much a moving dot smears along its motion. */\n  stretch?: number\n  glow?: number\n  shape?: Shape\n  /** Fade towards the edges. */\n  fade?: number\n  opacity?: number\n  /** Pop the dots in from the centre on mount. */\n  intro?: boolean\n  mouseInteraction?: boolean\n  clickShock?: boolean\n  paused?: boolean\n  /** Override the device pixel ratio (capped at 2 by default). */\n  dpr?: number\n}\n\ninterface Shock {\n  x: number\n  y: number\n  start: number\n}\n\nconst toRgb = (color: string): [number, number, number] => {\n  const match = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(String(color).trim())\n  if (!match) return [1, 1, 1]\n  const hex = match[1].length === 3 ? match[1].replace(/./g, (digit) => digit + digit) : match[1]\n  return [0, 2, 4].map((index) => parseInt(hex.slice(index, index + 2), 16) / 255) as [number, number, number]\n}\n\nconst smootherstep = (t: number) => {\n  const x = Math.min(1, Math.max(0, t))\n  return x * x * x * (x * (x * 6 - 15) + 10)\n}\n\nconst backOut = (t: number) => {\n  const s = 1.9\n  const x = t - 1\n  return 1 + x * x * ((s + 1) * x + s)\n}\n\nconst compile = (gl: WebGL2RenderingContext, type: number, source: string) => {\n  const shader = gl.createShader(type) as WebGLShader\n  gl.shaderSource(shader, source)\n  gl.compileShader(shader)\n  return shader\n}\n\nexport function DotGrid({\n  dotSize = 5,\n  gap = 18,\n  baseColor = \"#3a3446\",\n  activeColor = \"#ffffff\",\n  proximity = 140,\n  strength = 1,\n  bounce = 0.6,\n  tension = 0.4,\n  returnDuration = 0.8,\n  shockRadius = 320,\n  shockStrength = 5,\n  swell = 0.8,\n  stretch = 0.5,\n  glow = 0.5,\n  shape = \"circle\",\n  fade = 0,\n  opacity = 1,\n  intro = true,\n  mouseInteraction = true,\n  clickShock = true,\n  paused = false,\n  dpr,\n  className,\n  style,\n  ...rest\n}: DotGridProps) {\n  const rootRef = useRef<HTMLDivElement>(null)\n  const engineRef = useRef<{ rebuild: () => void; refresh: () => void } | null>(null)\n  const settings = {\n    dotSize,\n    gap,\n    baseColor,\n    activeColor,\n    proximity,\n    strength,\n    bounce,\n    tension,\n    returnDuration,\n    shockRadius,\n    shockStrength,\n    swell,\n    stretch,\n    glow,\n    shape,\n    fade,\n    opacity,\n    intro,\n    mouseInteraction,\n    clickShock,\n    paused,\n    dpr,\n  }\n  const settingsRef = useRef(settings)\n\n  useEffect(() => {\n    const root = rootRef.current\n    if (!root) return\n    // The canvas is made here, not in JSX: React Strict Mode runs this effect twice in development, and the\n    // context the first cleanup loses would come straight back from getContext() on the same element.\n    const canvas = document.createElement(\"canvas\")\n    canvas.className = \"pointer-events-none absolute inset-0 block h-full w-full\"\n    canvas.setAttribute(\"aria-hidden\", \"true\")\n    const gl = canvas.getContext(\"webgl2\", { alpha: true, premultipliedAlpha: true, antialias: false })\n    if (!gl) return\n    root.appendChild(canvas)\n\n    const program = gl.createProgram() as WebGLProgram\n    gl.attachShader(program, compile(gl, gl.VERTEX_SHADER, VERTEX))\n    gl.attachShader(program, compile(gl, gl.FRAGMENT_SHADER, FRAGMENT))\n    gl.linkProgram(program)\n    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) return\n\n    const uniforms: Record<string, WebGLUniformLocation | null> = {}\n    for (const name of [\n      \"uResolution\",\n      \"uSize\",\n      \"uSwell\",\n      \"uStretch\",\n      \"uGlow\",\n      \"uBase\",\n      \"uActive\",\n      \"uOpacity\",\n      \"uFade\",\n      \"uShape\",\n    ]) {\n      uniforms[name] = gl.getUniformLocation(program, name)\n    }\n\n    const vao = gl.createVertexArray()\n    gl.bindVertexArray(vao)\n    const cornerBuffer = gl.createBuffer()\n    gl.bindBuffer(gl.ARRAY_BUFFER, cornerBuffer)\n    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW)\n    gl.enableVertexAttribArray(0)\n    gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0)\n    const instanceBuffer = gl.createBuffer()\n    gl.bindBuffer(gl.ARRAY_BUFFER, instanceBuffer)\n    gl.enableVertexAttribArray(1)\n    gl.vertexAttribPointer(1, 4, gl.FLOAT, false, 24, 0)\n    gl.vertexAttribDivisor(1, 1)\n    gl.enableVertexAttribArray(2)\n    gl.vertexAttribPointer(2, 2, gl.FLOAT, false, 24, 16)\n    gl.vertexAttribDivisor(2, 1)\n    gl.bindVertexArray(null)\n\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n    const view = { width: 0, height: 0, left: 0, top: 0, ratio: 1 }\n    const grid = { count: 0, cols: 1, rows: 1, cell: 1, centerX: 0, centerY: 0, reach: 1 }\n    let rest = new Float32Array(0)\n    let offset = new Float32Array(0)\n    let velocity = new Float32Array(0)\n    let force = new Float32Array(0)\n    let next = new Float32Array(0)\n    let wave = new Float32Array(0)\n    let energy = new Float32Array(0)\n    let instances = new Float32Array(0)\n    const pointer = { x: 0, y: 0, vx: 0, vy: 0, time: 0, inside: false, presence: 0 }\n    const shocks: Shock[] = []\n    let introStart: number | null = settingsRef.current.intro && !reduced ? -1 : null\n    let visible = true\n    let raf = 0\n    let last = 0\n\n    const rebuild = () => {\n      const { dotSize: size, gap: spacing } = settingsRef.current\n      const cell = Math.max(2, size + spacing)\n      const cols = Math.max(1, Math.floor((view.width + spacing) / cell))\n      const rows = Math.max(1, Math.floor((view.height + spacing) / cell))\n      const startX = (view.width - (cols * cell - spacing)) / 2 + size / 2\n      const startY = (view.height - (rows * cell - spacing)) / 2 + size / 2\n      const count = cols * rows\n      rest = new Float32Array(count * 2)\n      offset = new Float32Array(count * 2)\n      velocity = new Float32Array(count * 2)\n      force = new Float32Array(count * 2)\n      next = new Float32Array(count * 2)\n      wave = new Float32Array(count * 4)\n      energy = new Float32Array(count)\n      instances = new Float32Array(count * 6)\n      for (let row = 0; row < rows; row++) {\n        for (let col = 0; col < cols; col++) {\n          const index = (row * cols + col) * 2\n          rest[index] = startX + col * cell\n          rest[index + 1] = startY + row * cell\n        }\n      }\n      Object.assign(grid, {\n        count,\n        cols,\n        rows,\n        cell,\n        centerX: view.width / 2,\n        centerY: view.height / 2,\n        reach: Math.hypot(view.width, view.height) / 2,\n      })\n      gl.bindBuffer(gl.ARRAY_BUFFER, instanceBuffer)\n      gl.bufferData(gl.ARRAY_BUFFER, instances.byteLength, gl.DYNAMIC_DRAW)\n    }\n\n    const resize = () => {\n      const rect = canvas.getBoundingClientRect()\n      const ratio = Math.min(settingsRef.current.dpr ?? window.devicePixelRatio ?? 1, 2)\n      const width = Math.max(1, Math.round(rect.width))\n      const height = Math.max(1, Math.round(rect.height))\n      Object.assign(view, { width, height, left: rect.left, top: rect.top, ratio })\n      canvas.width = Math.round(width * ratio)\n      canvas.height = Math.round(height * ratio)\n      rebuild()\n      render(performance.now())\n    }\n\n    const step = (dt: number, now: number) => {\n      const s = settingsRef.current\n      const motion = reduced ? 0 : 1\n      const zeta = 1 - 0.85 * Math.min(1, Math.max(0, s.bounce))\n      const omega = 4 / (zeta * Math.max(0.2, s.returnDuration))\n      const stiffness = omega * omega\n      const damping = 2 * zeta * omega\n      const radius = Math.max(1, s.proximity)\n      const speed = Math.hypot(pointer.vx, pointer.vy)\n      const drive = s.mouseInteraction && pointer.inside ? 6500 * Math.tanh(speed / 900) * s.strength * motion : 0\n      const dirX = speed > 0.001 ? pointer.vx / speed : 0\n      const dirY = speed > 0.001 ? pointer.vy / speed : 0\n      const width = grid.cell * 1.4\n      const travel = 650\n      const active = shocks.map((shock) => {\n        const front = ((now - shock.start) / 1000) * travel\n        const power = smootherstep(1 - front / Math.max(1, s.shockRadius))\n        return { ...shock, front, amplitude: power * s.shockStrength * grid.cell * 0.24 * motion }\n      })\n      const coupling = (Math.max(0, s.tension) * 900) ** 2 / (grid.cell * grid.cell)\n      const viscosity = Math.sqrt(coupling) * 0.06\n      const limit = Math.sqrt(stiffness + 8 * coupling) + damping + 8 * viscosity\n      const steps = Math.min(24, Math.max(1, Math.ceil(dt / Math.min(1 / 120, 1.2 / limit))))\n      const h = dt / steps\n      for (let i = 0; i < grid.count; i++) {\n        const index = i * 2\n        const rx = rest[index]\n        const ry = rest[index + 1]\n        let ax = 0\n        let ay = 0\n        if (drive > 0) {\n          const ex = rx + offset[index] - pointer.x\n          const ey = ry + offset[index + 1] - pointer.y\n          const distance = Math.hypot(ex, ey)\n          if (distance < radius) {\n            const weight = smootherstep(1 - distance / radius) * drive\n            const nx = distance > 0.001 ? ex / distance : 0\n            const ny = distance > 0.001 ? ey / distance : 0\n            ax += weight * (dirX + nx * 0.6)\n            ay += weight * (dirY + ny * 0.6)\n          }\n        }\n        let wx = 0\n        let wy = 0\n        let wvx = 0\n        let wvy = 0\n        for (const shock of active) {\n          if (shock.amplitude <= 0) continue\n          const ex = rx - shock.x\n          const ey = ry - shock.y\n          const distance = Math.hypot(ex, ey)\n          const u = (distance - shock.front) / width\n          if (Math.abs(u) < 3 && distance > 0.001) {\n            const nx = ex / distance\n            const ny = ey / distance\n            const profile = Math.exp(-u * u)\n            const push = shock.amplitude * profile\n            const rate = ((shock.amplitude * 2 * u * profile) / width) * travel\n            wx += nx * push\n            wy += ny * push\n            wvx += nx * rate\n            wvy += ny * rate\n            ax += nx * push * stiffness * 0.35\n            ay += ny * push * stiffness * 0.35\n          }\n        }\n        force[index] = ax\n        force[index + 1] = ay\n        const target = i * 4\n        wave[target] = wx\n        wave[target + 1] = wy\n        wave[target + 2] = wvx\n        wave[target + 3] = wvy\n      }\n      const { cols, rows } = grid\n      const stride = cols * 2\n      for (let n = 0; n < steps; n++) {\n        for (let row = 0; row < rows; row++) {\n          for (let col = 0; col < cols; col++) {\n            const index = (row * cols + col) * 2\n            let ax = force[index] - stiffness * offset[index] - damping * velocity[index]\n            let ay = force[index + 1] - stiffness * offset[index + 1] - damping * velocity[index + 1]\n            if (coupling > 0) {\n              let lx = -4 * offset[index]\n              let ly = -4 * offset[index + 1]\n              let mx = -4 * velocity[index]\n              let my = -4 * velocity[index + 1]\n              if (col > 0) {\n                lx += offset[index - 2]\n                ly += offset[index - 1]\n                mx += velocity[index - 2]\n                my += velocity[index - 1]\n              }\n              if (col < cols - 1) {\n                lx += offset[index + 2]\n                ly += offset[index + 3]\n                mx += velocity[index + 2]\n                my += velocity[index + 3]\n              }\n              if (row > 0) {\n                lx += offset[index - stride]\n                ly += offset[index - stride + 1]\n                mx += velocity[index - stride]\n                my += velocity[index - stride + 1]\n              }\n              if (row < rows - 1) {\n                lx += offset[index + stride]\n                ly += offset[index + stride + 1]\n                mx += velocity[index + stride]\n                my += velocity[index + stride + 1]\n              }\n              ax += coupling * lx + viscosity * mx\n              ay += coupling * ly + viscosity * my\n            }\n            next[index] = velocity[index] + ax * h\n            next[index + 1] = velocity[index + 1] + ay * h\n          }\n        }\n        for (let i = 0; i < offset.length; i++) {\n          velocity[i] = next[i]\n          offset[i] += velocity[i] * h\n        }\n      }\n      for (let i = 0; i < grid.count; i++) {\n        const index = i * 2\n        const target = i * 4\n        energy[i] = Math.min(\n          1,\n          (Math.hypot(velocity[index] + wave[target + 2], velocity[index + 1] + wave[target + 3]) / 380) * 0.85 +\n            (Math.hypot(offset[index] + wave[target], offset[index + 1] + wave[target + 1]) / (grid.cell * 2.2)) * 0.35,\n        )\n      }\n      for (let i = shocks.length - 1; i >= 0; i--) {\n        if (active[i].front > s.shockRadius + width * 3) shocks.splice(i, 1)\n      }\n      const decay = 1 - Math.exp(-dt / 0.08)\n      pointer.vx -= pointer.vx * decay\n      pointer.vy -= pointer.vy * decay\n      const presenceTarget = s.mouseInteraction && pointer.inside ? 1 : 0\n      pointer.presence += (presenceTarget - pointer.presence) * (1 - Math.exp(-dt / 0.2))\n    }\n\n    const render = (now: number) => {\n      const s = settingsRef.current\n      const radius = Math.max(1, s.proximity)\n      const introAge = introStart === null ? Infinity : introStart < 0 ? 0 : (now - introStart) / 1000\n      const introFront = introAge * grid.reach * 1.25\n      const introBand = grid.cell * 4\n      for (let i = 0; i < grid.count; i++) {\n        const index = i * 2\n        const source = i * 4\n        const x = rest[index] + offset[index] + wave[source]\n        const y = rest[index + 1] + offset[index + 1] + wave[source + 1]\n        let glowing = energy[i]\n        if (pointer.presence > 0.001) {\n          const distance = Math.hypot(x - pointer.x, y - pointer.y)\n          if (distance < radius)\n            glowing = Math.max(glowing, smootherstep(1 - distance / radius) * 0.4 * pointer.presence)\n        }\n        let scale = 1\n        if (introStart !== null) {\n          const distance = Math.hypot(rest[index] - grid.centerX, rest[index + 1] - grid.centerY)\n          const appear = Math.min(1, Math.max(0, (introFront - distance) / introBand))\n          scale = appear >= 1 ? 1 : Math.max(0, backOut(appear))\n          glowing = Math.max(glowing, Math.sin(appear * Math.PI) * 0.6)\n        }\n        const target = i * 6\n        instances[target] = x\n        instances[target + 1] = y\n        instances[target + 2] = velocity[index] + wave[source + 2]\n        instances[target + 3] = velocity[index + 1] + wave[source + 3]\n        instances[target + 4] = glowing\n        instances[target + 5] = scale\n      }\n      const base = toRgb(s.baseColor)\n      const accent = toRgb(s.activeColor)\n      gl.viewport(0, 0, canvas.width, canvas.height)\n      gl.clearColor(0, 0, 0, 0)\n      gl.clear(gl.COLOR_BUFFER_BIT)\n      gl.enable(gl.BLEND)\n      gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA)\n      gl.useProgram(program)\n      gl.uniform2f(uniforms.uResolution, view.width, view.height)\n      gl.uniform1f(uniforms.uSize, Math.max(0.5, s.dotSize))\n      gl.uniform1f(uniforms.uSwell, reduced ? 0 : Math.max(0, s.swell))\n      gl.uniform1f(uniforms.uStretch, reduced ? 0 : Math.max(0, s.stretch))\n      gl.uniform1f(uniforms.uGlow, Math.max(0, s.glow))\n      gl.uniform3f(uniforms.uBase, base[0], base[1], base[2])\n      gl.uniform3f(uniforms.uActive, accent[0], accent[1], accent[2])\n      gl.uniform1f(uniforms.uOpacity, Math.min(1, Math.max(0, s.opacity)))\n      gl.uniform1f(uniforms.uFade, Math.min(1, Math.max(0, s.fade)))\n      gl.uniform1i(uniforms.uShape, s.shape === \"square\" ? 1 : 0)\n      gl.bindVertexArray(vao)\n      gl.bindBuffer(gl.ARRAY_BUFFER, instanceBuffer)\n      gl.bufferSubData(gl.ARRAY_BUFFER, 0, instances)\n      gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, grid.count)\n      gl.bindVertexArray(null)\n    }\n\n    const settled = (now: number) => {\n      if (shocks.length) return false\n      if (introStart !== null) {\n        if (introStart < 0 || (now - introStart) / 1000 < 1.6) return false\n        introStart = null\n      }\n      if (Math.abs(pointer.vx) + Math.abs(pointer.vy) > 1) return false\n      const presenceTarget = settingsRef.current.mouseInteraction && pointer.inside ? 1 : 0\n      if (Math.abs(presenceTarget - pointer.presence) > 0.002) return false\n      for (let i = 0; i < offset.length; i++) {\n        if (Math.abs(offset[i]) > 0.02 || Math.abs(velocity[i]) > 0.5) return false\n      }\n      return true\n    }\n\n    const frame = (now: number) => {\n      raf = 0\n      if (settingsRef.current.paused || !visible) return\n      if (introStart !== null && introStart < 0) introStart = now\n      const dt = Math.min(0.05, (now - last) / 1000 || 0)\n      last = now\n      step(dt, now)\n      render(now)\n      if (!settled(now)) raf = requestAnimationFrame(frame)\n    }\n\n    const wake = () => {\n      if (raf || settingsRef.current.paused || !visible) return\n      last = performance.now()\n      raf = requestAnimationFrame(frame)\n    }\n\n    const locate = (event: PointerEvent): [number, number] => {\n      const rect = canvas.getBoundingClientRect()\n      view.left = rect.left\n      view.top = rect.top\n      return [event.clientX - rect.left, event.clientY - rect.top]\n    }\n\n    const onMove = (event: PointerEvent) => {\n      if (!settingsRef.current.mouseInteraction) return\n      const [x, y] = locate(event)\n      const inside = x >= 0 && y >= 0 && x <= view.width && y <= view.height\n      const time = event.timeStamp || performance.now()\n      if (inside && pointer.inside && pointer.time) {\n        const elapsed = Math.max(4, time - pointer.time) / 1000\n        const vx = (x - pointer.x) / elapsed\n        const vy = (y - pointer.y) / elapsed\n        pointer.vx += (vx - pointer.vx) * 0.6\n        pointer.vy += (vy - pointer.vy) * 0.6\n      } else {\n        pointer.vx = 0\n        pointer.vy = 0\n      }\n      pointer.x = x\n      pointer.y = y\n      pointer.time = time\n      if (inside !== pointer.inside || inside) {\n        pointer.inside = inside\n        wake()\n      }\n    }\n\n    const onLeave = () => {\n      pointer.inside = false\n      pointer.time = 0\n      wake()\n    }\n\n    const onOut = (event: PointerEvent) => {\n      if (!event.relatedTarget) onLeave()\n    }\n\n    const onDown = (event: PointerEvent) => {\n      if (!settingsRef.current.clickShock || reduced) return\n      const [x, y] = locate(event)\n      if (x < 0 || y < 0 || x > view.width || y > view.height) return\n      shocks.push({ x, y, start: performance.now() })\n      if (shocks.length > 6) shocks.shift()\n      wake()\n    }\n\n    window.addEventListener(\"pointermove\", onMove, { passive: true })\n    window.addEventListener(\"pointerdown\", onDown, { passive: true })\n    window.addEventListener(\"pointerout\", onOut)\n    window.addEventListener(\"blur\", onLeave)\n\n    const resizeObserver = new ResizeObserver(() => {\n      resize()\n      wake()\n    })\n    resizeObserver.observe(canvas)\n\n    const visibilityObserver = new IntersectionObserver(([entry]) => {\n      visible = entry.isIntersecting\n      if (visible) wake()\n    })\n    visibilityObserver.observe(canvas)\n\n    const onVisibility = () => {\n      if (document.hidden) {\n        cancelAnimationFrame(raf)\n        raf = 0\n      } else {\n        wake()\n      }\n    }\n    document.addEventListener(\"visibilitychange\", onVisibility)\n\n    resize()\n    wake()\n\n    engineRef.current = {\n      rebuild: () => {\n        rebuild()\n        render(performance.now())\n        wake()\n      },\n      refresh: () => {\n        render(performance.now())\n        wake()\n      },\n    }\n\n    return () => {\n      cancelAnimationFrame(raf)\n      resizeObserver.disconnect()\n      visibilityObserver.disconnect()\n      window.removeEventListener(\"pointermove\", onMove)\n      window.removeEventListener(\"pointerdown\", onDown)\n      window.removeEventListener(\"pointerout\", onOut)\n      window.removeEventListener(\"blur\", onLeave)\n      document.removeEventListener(\"visibilitychange\", onVisibility)\n      gl.deleteBuffer(cornerBuffer)\n      gl.deleteBuffer(instanceBuffer)\n      gl.deleteVertexArray(vao)\n      gl.deleteProgram(program)\n      gl.getExtension(\"WEBGL_lose_context\")?.loseContext()\n      canvas.remove()\n      engineRef.current = null\n    }\n  }, [])\n\n  // Props flow through a ref, so the WebGL setup effect above runs exactly once. The two effects\n  // below then only rebuild the grid or redraw. They never tear the context down.\n  useEffect(() => {\n    settingsRef.current = settings\n  })\n\n  useEffect(() => {\n    engineRef.current?.rebuild()\n  }, [dotSize, gap])\n\n  useEffect(() => {\n    engineRef.current?.refresh()\n  }, [baseColor, activeColor, swell, stretch, glow, shape, fade, opacity, paused, proximity, mouseInteraction])\n\n  return (\n    <div\n      ref={rootRef}\n      data-slot=\"dot-grid\"\n      className={cn(\"relative h-full w-full overflow-hidden\", className)}\n      style={style}\n      {...rest}\n    />\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "meta": {
    "added": "2026-10-21"
  },
  "categories": [
    "background"
  ],
  "type": "registry:ui"
}