{
  "name": "rain-glass-background",
  "type": "registry:component",
  "dependencies": [],
  "files": [
    {
      "type": "registry:component",
      "content": "\"use client\";\n\n/**\n * @author: @dorianbaffier\n * @description: Rain Glass Background\n * @version: 1.0.0\n * @date: 2026-10-22\n * @license: MIT\n * @website: https://kokonutui.com\n * @github: https://github.com/kokonut-labs/kokonutui\n */\n\nimport { type ReactNode, useEffect, useRef } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface RainGlassBackgroundProps {\n  /** The view behind the window. Served with CORS so WebGL can read it. */\n  src?: string;\n  title?: string;\n  /** Content under the headline. */\n  children?: ReactNode;\n  /** How hard it rains, from 0 (a few drops) to 1 (a downpour). */\n  rain?: number;\n  /** Set false for a window you can't wipe. */\n  interactive?: boolean;\n  className?: string;\n}\n\nconst DEFAULT_SRC =\n  \"https://images.unsplash.com/photo-1501785888041-af3ef285b470?w=1800&q=80&auto=format&fit=crop\";\n\n// The drop map is drawn at three quarter size, the fog map at half.\nconst WATER_SCALE = 0.75;\nconst FOG_SCALE = 0.5;\n// The most drops on the pane at once.\nconst MAX_DROPS = 140;\n// A drop this big (css px) gets heavy enough to run.\nconst RUN_AT = 7.5;\n// How long a wiped patch takes to fog over again, in seconds.\nconst FOG_BACK = 9;\nconst FOG_STEP = 0.25;\n// How much the fog hides the view, 0 to 1.\nconst FOG = 0.6;\nconst WIPE_WIDTH = 64;\n\nconst VERTEX = `#version 300 es\nin vec2 aPos;\nout vec2 vUv;\nvoid main() {\n  vUv = aPos * 0.5 + 0.5;\n  gl_Position = vec4(aPos, 0.0, 1.0);\n}`;\n\nconst FRAGMENT = `#version 300 es\nprecision highp float;\nin vec2 vUv;\nout vec4 o;\nuniform sampler2D uScene;\nuniform sampler2D uWater;\nuniform sampler2D uClear;\nuniform vec4 uCover;\nuniform vec2 uRes;\nuniform vec3 uFog;\nuniform float uFogAmount;\n\nvec2 hash(vec2 p) {\n  p = vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3)));\n  return fract(sin(p) * 43758.5453);\n}\n\n// Fine mist on the pane, one droplet per small cell, drawn here so it stays\n// sharp at any size.\nfloat mist(vec2 px, out vec2 normal) {\n  vec2 cell = floor(px / 7.0);\n  if (hash(cell + 7.7).x > 0.55) {\n    normal = vec2(0.0);\n    return 0.0;\n  }\n  vec2 h = hash(cell);\n  vec2 center = (cell + 0.2 + 0.6 * h) * 7.0;\n  float r = 0.6 + 1.7 * pow(hash(cell + 3.1).x, 3.0);\n  vec2 d = (px - center) / r;\n  normal = d;\n  return sqrt(max(0.0, 1.0 - dot(d, d)));\n}\n\nvec3 scene(vec2 uv, float lod) {\n  return textureLod(uScene, uv * uCover.xy + uCover.zw, lod).rgb;\n}\n\nfloat shine(vec2 n, float thick) {\n  vec3 l = normalize(vec3(-0.45, 0.55, 0.7));\n  return pow(max(dot(normalize(vec3(n.x, -n.y, thick + 0.15)), l), 0.0), 36.0);\n}\n\nvoid main() {\n  vec2 uv = vUv;\n  vec4 w = texture(uWater, uv);\n  vec2 n = w.rg * 2.0 - 1.0;\n  float thick = w.b;\n  float clear = texture(uClear, uv).r;\n\n  // Fogged glass: the view goes soft and pale. Wiped glass is clear.\n  vec3 soft = mix(scene(uv, 5.0), uFog, uFogAmount);\n  vec3 glass = mix(soft, scene(uv, 0.6), clear);\n\n  // A drop is a tiny lens: it shows the view sharp and upside down.\n  vec2 aspect = vec2(uRes.y / uRes.x, 1.0);\n  vec2 bent = uv + vec2(-n.x, n.y) * aspect * 0.07 * (0.5 + thick);\n  vec3 drop = mix(scene(bent, 0.4), soft, 0.18);\n  drop *= mix(0.8, 1.05, smoothstep(0.0, 0.6, thick));\n  drop += shine(n, thick) * 0.55;\n  float a = smoothstep(0.02, 0.18, thick);\n  vec3 col = mix(glass, drop, a);\n\n  // Fine mist: beads too small to show the view, they just catch light.\n  // Wiping takes them off, and they come back with the fog.\n  vec2 mn;\n  float m = mist(vec2(uv.x, 1.0 - uv.y) * uRes, mn) * (1.0 - clear);\n  vec3 bead = mix(glass, scene(uv - mn * 0.004, 1.0), 0.5);\n  bead = bead * mix(0.92, 1.06, m) + shine(mn, m) * 0.35;\n  col = mix(col, bead, smoothstep(0.05, 0.4, m) * (1.0 - a) * 0.75);\n\n  o = vec4(col, 1.0);\n}`;\n\nfunction compile(gl: WebGL2RenderingContext, type: number, source: string) {\n  const shader = gl.createShader(type);\n  if (!shader) {\n    return null;\n  }\n  gl.shaderSource(shader, source);\n  gl.compileShader(shader);\n  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n    gl.deleteShader(shader);\n    return null;\n  }\n  return shader;\n}\n\n/** Parses any CSS color, oklch included, by painting it into one pixel. */\nfunction toRgb(color: string, probe: CanvasRenderingContext2D) {\n  probe.clearRect(0, 0, 1, 1);\n  probe.fillStyle = color;\n  probe.fillRect(0, 0, 1, 1);\n  const [r, g, b] = probe.getImageData(0, 0, 1, 1).data;\n  return [r / 255, g / 255, b / 255];\n}\n\n/**\n * One drop as a normal map: red and green tilt the surface, blue is how\n * thick the water is. Drawn once, stamped for every drop.\n */\nfunction dropSprite() {\n  const size = 64;\n  const canvas = document.createElement(\"canvas\");\n  canvas.width = size;\n  canvas.height = size;\n  const ctx = canvas.getContext(\"2d\");\n  if (!ctx) {\n    return canvas;\n  }\n  const image = ctx.createImageData(size, size);\n  for (let y = 0; y < size; y++) {\n    for (let x = 0; x < size; x++) {\n      const dx = ((x + 0.5) / size) * 2 - 1;\n      const dy = ((y + 0.5) / size) * 2 - 1;\n      const d = Math.hypot(dx, dy);\n      const i = (y * size + x) * 4;\n      const edge = Math.min(1, Math.max(0, (1 - d) / 0.12));\n      image.data[i] = 128 + dx * 127;\n      image.data[i + 1] = 128 + dy * 127;\n      image.data[i + 2] = Math.sqrt(Math.max(0, 1 - d * d)) * 255;\n      image.data[i + 3] = edge * 255;\n    }\n  }\n  ctx.putImageData(image, 0, 0);\n  return canvas;\n}\n\ninterface Drop {\n  x: number;\n  y: number;\n  r: number;\n  vy: number;\n  /** Sideways drift while running, so the path isn't a ruler line. */\n  drift: number;\n  /** Distance run since the last trail droplet. */\n  trail: number;\n}\n\ninterface Pane {\n  resize: (w: number, h: number, cssW: number, cssH: number) => void;\n  setScene: (image: HTMLImageElement, cssW: number, cssH: number) => void;\n  setCover: (cssW: number, cssH: number) => void;\n  setFog: (rgb: number[], amount: number) => void;\n  upload: (water: HTMLCanvasElement, clear: HTMLCanvasElement) => void;\n  draw: () => void;\n  destroy: () => void;\n}\n\nfunction texture(gl: WebGL2RenderingContext, unit: number) {\n  const tex = gl.createTexture();\n  gl.activeTexture(gl.TEXTURE0 + unit);\n  gl.bindTexture(gl.TEXTURE_2D, tex);\n  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);\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  gl.texImage2D(\n    gl.TEXTURE_2D,\n    0,\n    gl.RGBA,\n    1,\n    1,\n    0,\n    gl.RGBA,\n    gl.UNSIGNED_BYTE,\n    new Uint8Array([128, 128, 0, 255])\n  );\n  return tex;\n}\n\nfunction createPane(gl: WebGL2RenderingContext): Pane | null {\n  const vertex = compile(gl, gl.VERTEX_SHADER, VERTEX);\n  const fragment = compile(gl, gl.FRAGMENT_SHADER, FRAGMENT);\n  const program = gl.createProgram();\n  if (!(vertex && fragment && program)) {\n    return null;\n  }\n  gl.attachShader(program, vertex);\n  gl.attachShader(program, fragment);\n  gl.bindAttribLocation(program, 0, \"aPos\");\n  gl.linkProgram(program);\n  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n    return null;\n  }\n  const at = (name: string) => gl.getUniformLocation(program, name);\n\n  const quad = gl.createBuffer();\n  gl.bindBuffer(gl.ARRAY_BUFFER, quad);\n  gl.bufferData(\n    gl.ARRAY_BUFFER,\n    new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),\n    gl.STATIC_DRAW\n  );\n  gl.enableVertexAttribArray(0);\n  gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);\n\n  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);\n  const sceneTex = texture(gl, 0);\n  const waterTex = texture(gl, 1);\n  const clearTex = texture(gl, 2);\n  let imageAspect = 1;\n\n  // biome-ignore lint/correctness/useHookAtTopLevel: WebGL's useProgram, not a React hook.\n  gl.useProgram(program);\n  gl.uniform1i(at(\"uScene\"), 0);\n  gl.uniform1i(at(\"uWater\"), 1);\n  gl.uniform1i(at(\"uClear\"), 2);\n  gl.uniform4f(at(\"uCover\"), 1, 1, 0, 0);\n\n  const setCover = (cssW: number, cssH: number) => {\n    // Cover the pane, like object-fit: cover.\n    const ratio = cssW / cssH / imageAspect;\n    const sx = ratio > 1 ? 1 : ratio;\n    const sy = ratio > 1 ? 1 / ratio : 1;\n    gl.uniform4f(at(\"uCover\"), sx, sy, 0.5 - sx / 2, 0.5 - sy / 2);\n  };\n\n  return {\n    resize: (w, h, cssW, cssH) => {\n      gl.viewport(0, 0, w, h);\n      gl.uniform2f(at(\"uRes\"), cssW, cssH);\n    },\n    setScene: (image, cssW, cssH) => {\n      imageAspect = image.naturalWidth / image.naturalHeight;\n      gl.activeTexture(gl.TEXTURE0);\n      gl.bindTexture(gl.TEXTURE_2D, sceneTex);\n      gl.texImage2D(\n        gl.TEXTURE_2D,\n        0,\n        gl.RGBA,\n        gl.RGBA,\n        gl.UNSIGNED_BYTE,\n        image\n      );\n      gl.generateMipmap(gl.TEXTURE_2D);\n      gl.texParameteri(\n        gl.TEXTURE_2D,\n        gl.TEXTURE_MIN_FILTER,\n        gl.LINEAR_MIPMAP_LINEAR\n      );\n      setCover(cssW, cssH);\n    },\n    setCover,\n    setFog: (rgb, amount) => {\n      gl.uniform3f(at(\"uFog\"), rgb[0], rgb[1], rgb[2]);\n      gl.uniform1f(at(\"uFogAmount\"), amount);\n    },\n    upload: (water, clear) => {\n      gl.activeTexture(gl.TEXTURE1);\n      gl.bindTexture(gl.TEXTURE_2D, waterTex);\n      gl.texImage2D(\n        gl.TEXTURE_2D,\n        0,\n        gl.RGBA,\n        gl.RGBA,\n        gl.UNSIGNED_BYTE,\n        water\n      );\n      gl.activeTexture(gl.TEXTURE2);\n      gl.bindTexture(gl.TEXTURE_2D, clearTex);\n      gl.texImage2D(\n        gl.TEXTURE_2D,\n        0,\n        gl.RGBA,\n        gl.RGBA,\n        gl.UNSIGNED_BYTE,\n        clear\n      );\n    },\n    draw: () => {\n      gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);\n    },\n    destroy: () => {\n      gl.deleteTexture(sceneTex);\n      gl.deleteTexture(waterTex);\n      gl.deleteTexture(clearTex);\n      gl.deleteBuffer(quad);\n      gl.deleteProgram(program);\n      gl.deleteShader(vertex);\n      gl.deleteShader(fragment);\n    },\n  };\n}\n\nfunction rainDrop(width: number, height: number): Drop {\n  return {\n    x: Math.random() * width,\n    y: Math.random() * height * 0.95,\n    r: 2.5 + Math.random() ** 2 * 6,\n    vy: 0,\n    drift: (Math.random() - 0.5) * 0.6,\n    trail: 0,\n  };\n}\n\n/** A running drop takes in whatever it runs into. */\nfunction swallow(d: Drop, drops: Drop[]) {\n  for (const other of drops) {\n    if (other === d || other.r <= 0) {\n      continue;\n    }\n    const reach = (d.r + other.r) * 0.8;\n    if (\n      Math.abs(other.x - d.x) < reach &&\n      Math.abs(other.y - d.y) < reach &&\n      Math.hypot(other.x - d.x, other.y - d.y) < reach\n    ) {\n      d.r = Math.min(16, Math.hypot(d.r, other.r * 0.9));\n      other.r = 0;\n    }\n  }\n}\n\n/** Moves the drops on: heavy ones run, leave a trail and swallow others. */\nfunction stepDrops(\n  drops: Drop[],\n  dt: number,\n  height: number,\n  clear: CanvasRenderingContext2D\n) {\n  const fresh: Drop[] = [];\n  for (const d of drops) {\n    if (d.r < RUN_AT) {\n      continue;\n    }\n    // Heavier drops run faster; friction makes them stutter.\n    const pull = 140 + d.r * 22;\n    d.vy += (pull - d.vy) * Math.min(1, dt * (Math.random() < 0.06 ? 0.4 : 3));\n    const dy = d.vy * dt;\n    d.y += dy;\n    d.x += d.drift * dy * 0.25 + (Math.random() - 0.5) * 0.4;\n    d.trail += dy;\n    // Running water wipes the fog in a thin line behind it.\n    clear.beginPath();\n    clear.arc(d.x * FOG_SCALE, d.y * FOG_SCALE, d.r * 0.7 * FOG_SCALE, 0, 7);\n    clear.fill();\n    // Now and then it leaves a droplet behind, a little water lost.\n    if (d.trail > d.r * 3) {\n      d.trail = -Math.random() * d.r * 4;\n      d.r *= 0.985;\n      fresh.push({\n        x: d.x + (Math.random() - 0.5) * 2,\n        y: d.y - d.r * 1.2,\n        r: d.r * (0.14 + Math.random() * 0.14),\n        vy: 0,\n        drift: 0,\n        trail: 0,\n      });\n    }\n    swallow(d, drops);\n    if (d.y - d.r > height) {\n      d.r = 0;\n    }\n  }\n  return fresh;\n}\n\n/** Stamps every drop into the water map. */\nfunction paintWater(\n  ctx: CanvasRenderingContext2D,\n  drops: Drop[],\n  sprite: HTMLCanvasElement,\n  width: number,\n  height: number\n) {\n  ctx.setTransform(1, 0, 0, 1, 0, 0);\n  ctx.fillStyle = \"rgb(128, 128, 0)\";\n  ctx.fillRect(0, 0, width * WATER_SCALE, height * WATER_SCALE);\n  ctx.setTransform(WATER_SCALE, 0, 0, WATER_SCALE, 0, 0);\n  for (const d of drops) {\n    // A running drop pulls into a teardrop.\n    const tall = d.vy > 20 ? 1.25 : 1;\n    ctx.drawImage(sprite, d.x - d.r, d.y - d.r * tall, d.r * 2, d.r * 2 * tall);\n  }\n}\n\nexport default function RainGlassBackground({\n  src = DEFAULT_SRC,\n  title = \"Stay in when it rains.\",\n  children,\n  rain = 0.6,\n  interactive = true,\n  className,\n}: RainGlassBackgroundProps) {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const fogRef = useRef<HTMLSpanElement>(null);\n\n  useEffect(() => {\n    const root = rootRef.current;\n    const canvas = canvasRef.current;\n    const gl = canvas?.getContext(\"webgl2\", {\n      alpha: false,\n      antialias: false,\n      depth: false,\n    });\n    const probe = document\n      .createElement(\"canvas\")\n      .getContext(\"2d\", { willReadFrequently: true });\n    const pane = gl ? createPane(gl) : null;\n    const waterCanvas = document.createElement(\"canvas\");\n    const clearCanvas = document.createElement(\"canvas\");\n    const water = waterCanvas.getContext(\"2d\");\n    const clear = clearCanvas.getContext(\"2d\");\n    if (!(root && canvas && probe && pane && water && clear)) {\n      return;\n    }\n    const reduceMotion = window.matchMedia(\n      \"(prefers-reduced-motion: reduce)\"\n    ).matches;\n    const sprite = dropSprite();\n    let drops: Drop[] = [];\n    let width = 0;\n    let height = 0;\n    let raf = 0;\n    let last = 0;\n    let visible = true;\n    let spawn = 0;\n    let fogClock = 0;\n    let colorKey = \"\";\n    const hand = { down: false, x: 0, y: 0 };\n\n    const readFog = () => {\n      const color = getComputedStyle(fogRef.current as Element).color;\n      if (color === colorKey) {\n        return false;\n      }\n      colorKey = color;\n      pane.setFog(toRgb(color, probe), FOG);\n      return true;\n    };\n\n    const render = () => {\n      paintWater(water, drops, sprite, width, height);\n      pane.upload(waterCanvas, clearCanvas);\n      pane.draw();\n    };\n\n    const build = () => {\n      const rect = root.getBoundingClientRect();\n      const resized =\n        Math.abs(rect.width - width) > 1 || Math.abs(rect.height - height) > 1;\n      width = rect.width;\n      height = rect.height;\n      if (width < 2 || height < 2) {\n        return;\n      }\n      const dpr = Math.min(window.devicePixelRatio || 1, 1.5);\n      canvas.width = Math.round(width * dpr);\n      canvas.height = Math.round(height * dpr);\n      pane.resize(canvas.width, canvas.height, width, height);\n      pane.setCover(width, height);\n      waterCanvas.width = Math.round(width * WATER_SCALE);\n      waterCanvas.height = Math.round(height * WATER_SCALE);\n      clearCanvas.width = Math.round(width * FOG_SCALE);\n      clearCanvas.height = Math.round(height * FOG_SCALE);\n      clear.fillStyle = \"black\";\n      clear.fillRect(0, 0, clearCanvas.width, clearCanvas.height);\n      if (resized || drops.length === 0) {\n        drops = [];\n        const settled = Math.round(20 + rain * 40);\n        for (let i = 0; i < settled; i++) {\n          const d = rainDrop(width, height);\n          d.r = Math.min(d.r, RUN_AT - 0.5);\n          drops.push(d);\n        }\n      }\n      render();\n    };\n\n    const frame = (now: number) => {\n      raf = 0;\n      if (!visible) {\n        return;\n      }\n      const dt = Math.min((now - (last || now - 16)) / 1000, 1 / 20);\n      last = now;\n      // New drops land; now and then one lands on another and gets heavy.\n      spawn += dt * (4 + rain * 22);\n      while (spawn > 1) {\n        spawn -= 1;\n        if (drops.length < MAX_DROPS) {\n          drops.push(rainDrop(width, height));\n        } else {\n          const d = drops[Math.floor(Math.random() * drops.length)];\n          d.r = Math.min(14, d.r + 1.2);\n        }\n      }\n      const fresh = stepDrops(drops, dt, height, clear);\n      drops = drops.filter((d) => d.r > 0.6).concat(fresh);\n      // Wiped glass fogs over again, slowly. In steps, so the 8-bit fog\n      // map keeps fading instead of getting stuck at a faint grey.\n      fogClock += dt;\n      if (fogClock > FOG_STEP) {\n        clear.fillStyle = `rgba(0, 0, 0, ${(fogClock / FOG_BACK) * 2.4})`;\n        clear.fillRect(0, 0, clearCanvas.width, clearCanvas.height);\n        clear.fillStyle = \"white\";\n        fogClock = 0;\n      }\n      render();\n      raf = requestAnimationFrame(frame);\n    };\n    const start = () => {\n      if (!raf && visible && !reduceMotion) {\n        last = 0;\n        raf = requestAnimationFrame(frame);\n      }\n    };\n\n    readFog();\n    build();\n    canvas.style.opacity = \"1\";\n    start();\n\n    const image = new Image();\n    image.crossOrigin = \"anonymous\";\n    image.decoding = \"async\";\n    image.onload = () => {\n      pane.setScene(image, width, height);\n      root.dataset.loaded = \"true\";\n      if (reduceMotion) {\n        render();\n      }\n    };\n    image.src = src;\n\n    const resizeObserver = new ResizeObserver(() => build());\n    resizeObserver.observe(root);\n    // A tab coming back must not wake a background that's scrolled away.\n    let onScreen = true;\n    const intersection = new IntersectionObserver(([entry]) => {\n      onScreen = entry.isIntersecting;\n      visible = onScreen && document.visibilityState === \"visible\";\n      start();\n    });\n    intersection.observe(root);\n    const onVisibility = () => {\n      visible = onScreen && document.visibilityState === \"visible\";\n      start();\n    };\n    document.addEventListener(\"visibilitychange\", onVisibility);\n    const colorTimer = window.setInterval(() => {\n      if (!visible) {\n        return;\n      }\n      if (readFog() && reduceMotion) {\n        render();\n      }\n    }, 500);\n\n    const local = (event: PointerEvent) => {\n      const rect = root.getBoundingClientRect();\n      return { x: event.clientX - rect.left, y: event.clientY - rect.top };\n    };\n    // Dragging wipes the glass like the side of a hand: the fog and the\n    // drops under it go.\n    const wipe = (x: number, y: number) => {\n      clear.strokeStyle = \"white\";\n      clear.lineCap = \"round\";\n      clear.lineWidth = WIPE_WIDTH * FOG_SCALE;\n      clear.filter = \"blur(3px)\";\n      clear.beginPath();\n      clear.moveTo(hand.x * FOG_SCALE, hand.y * FOG_SCALE);\n      clear.lineTo(x * FOG_SCALE, y * FOG_SCALE);\n      clear.stroke();\n      clear.filter = \"none\";\n      const reach = WIPE_WIDTH / 2;\n      for (const d of drops) {\n        if (Math.hypot(d.x - x, d.y - y) < reach + d.r * 0.5) {\n          d.r = 0;\n        }\n      }\n      hand.x = x;\n      hand.y = y;\n      if (reduceMotion) {\n        render();\n      }\n    };\n    const onDown = (event: PointerEvent) => {\n      if ((event.target as HTMLElement).closest(\"a, button, input, textarea\")) {\n        return;\n      }\n      const p = local(event);\n      hand.down = true;\n      hand.x = p.x;\n      hand.y = p.y;\n      wipe(p.x, p.y);\n    };\n    const onMove = (event: PointerEvent) => {\n      if (hand.down) {\n        const p = local(event);\n        wipe(p.x, p.y);\n      }\n    };\n    const onUp = () => {\n      hand.down = false;\n    };\n    if (interactive) {\n      root.addEventListener(\"pointerdown\", onDown);\n      root.addEventListener(\"pointermove\", onMove);\n      window.addEventListener(\"pointerup\", onUp);\n      window.addEventListener(\"pointercancel\", onUp);\n    }\n\n    return () => {\n      cancelAnimationFrame(raf);\n      image.onload = null;\n      window.clearInterval(colorTimer);\n      resizeObserver.disconnect();\n      intersection.disconnect();\n      document.removeEventListener(\"visibilitychange\", onVisibility);\n      root.removeEventListener(\"pointerdown\", onDown);\n      root.removeEventListener(\"pointermove\", onMove);\n      window.removeEventListener(\"pointerup\", onUp);\n      window.removeEventListener(\"pointercancel\", onUp);\n      pane.destroy();\n    };\n  }, [src, rain, interactive]);\n\n  return (\n    <div\n      className={cn(\n        \"relative min-h-[560px] w-full touch-pan-y select-none overflow-hidden bg-zinc-200 text-zinc-900 dark:bg-zinc-900 dark:text-zinc-50\",\n        className\n      )}\n      ref={rootRef}\n    >\n      {/* Theme color for the shader: the fog on the glass. */}\n      <span\n        className=\"hidden text-[#e9edef] dark:text-[#14181b]\"\n        ref={fogRef}\n      />\n      <div aria-hidden=\"true\" className=\"pointer-events-none absolute inset-0\">\n        <canvas\n          className=\"h-full w-full opacity-0 transition-opacity duration-700 ease-out\"\n          ref={canvasRef}\n        />\n      </div>\n      <div className=\"relative flex min-h-[560px] flex-col items-center justify-center gap-6 px-6 py-16 text-center\">\n        <h2 className=\"max-w-3xl text-balance font-semibold text-5xl tracking-[-0.045em] sm:text-7xl\">\n          {title}\n        </h2>\n        {children === undefined ? <DefaultContent /> : children}\n      </div>\n    </div>\n  );\n}\n\nfunction DefaultContent() {\n  return (\n    <>\n      <p className=\"max-w-md text-balance text-base text-zinc-700 leading-relaxed dark:text-zinc-300\">\n        Cabins two hours from the city, with a wood stove, a deep bath and no\n        signal. Wipe the glass to look outside.\n      </p>\n      <div className=\"flex items-center gap-2\">\n        <a\n          className=\"inline-flex h-10 items-center rounded-full bg-zinc-900 px-5 font-medium text-sm text-white transition-colors duration-150 hover:bg-zinc-700 dark:bg-zinc-50 dark:text-zinc-900 dark:hover:bg-zinc-200\"\n          href=\"#book\"\n        >\n          Find a weekend\n        </a>\n        <a\n          className=\"inline-flex h-10 items-center rounded-full px-4 font-medium text-sm text-zinc-700 transition-colors duration-150 hover:bg-zinc-900/5 hover:text-zinc-900 dark:text-zinc-300 dark:hover:bg-white/10 dark:hover:text-zinc-50\"\n          href=\"#cabins\"\n        >\n          See the cabins\n        </a>\n      </div>\n    </>\n  );\n}\n",
      "path": "/components/kokonutui/rain-glass-background.tsx",
      "target": "components/kokonutui/rain-glass-background.tsx"
    }
  ]
}