{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "RippleDistortion-JS-CSS",
	"title": "RippleDistortion",
	"description": "Pointer-driven water displacement that warps content and leaves a decaying wake.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:file",
			"path": "RippleDistortion.css",
			"target": "@components/RippleDistortion.css",
			"content": ".ripple-distortion {\n  position: relative;\n  width: 100%;\n  height: 100%;\n  overflow: hidden;\n  background: #000;\n}\n\n.ripple-distortion canvas {\n  display: block;\n  width: 100%;\n  height: 100%;\n}\n"
		},
		{
			"type": "registry:component",
			"path": "RippleDistortion.jsx",
			"content": "import { useEffect, useRef } from 'react';\nimport { Renderer, Program, Mesh, Geometry, Triangle, Texture, RenderTarget } from 'ogl';\nimport './RippleDistortion.css';\n\nconst MAX_WAVES = 100;\nconst QUALITY_SCALE = { low: 0.4, medium: 0.7, high: 1 };\nconst START_SCALE = 1.5;\nconst LIFE_CONSTANT = Math.log(500);\n\nconst waveVertex = `\nprecision highp float;\n\nattribute vec2 position;\nattribute vec2 uv;\nattribute vec2 iOffset;\nattribute vec2 iScale;\nattribute float iOpacity;\n\nvarying vec2 vUv;\nvarying float vOpacity;\n\nvoid main() {\n  vUv = uv;\n  vOpacity = iOpacity;\n  gl_Position = vec4(iOffset + position * iScale, 0.0, 1.0);\n}\n`;\n\nconst waveFragment = `\nprecision highp float;\n\nvarying vec2 vUv;\nvarying float vOpacity;\n\nuniform float uRings;\n\nconst float PI = 3.141592653589793;\nconst float EDGE = 0.006737947;\n\nvoid main() {\n  vec2 p = vUv * 2.0 - 1.0;\n  float r = dot(p, p);\n  if (r > 1.0) discard;\n\n  float brush = (exp(-r * 5.0) - EDGE) / (1.0 - EDGE);\n\n  brush *= 0.55 + 0.45 * cos(sqrt(r) * PI * 2.0 * uRings);\n\n  gl_FragColor = vec4(vec3(brush * vOpacity * vOpacity), 1.0);\n}\n`;\n\nconst screenVertex = `\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\nconst compositeFragment = `\nprecision highp float;\n\nvarying vec2 vUv;\n\nuniform sampler2D uTexture;\nuniform sampler2D uDisplacement;\nuniform vec2 uResolution;\nuniform vec2 uTextureSize;\nuniform vec2 uTexel;\nuniform vec3 uTint;\nuniform vec3 uHighlight;\nuniform float uStrength;\nuniform float uSwirl;\nuniform float uDispersion;\nuniform float uGlint;\nuniform float uTintAmount;\nuniform float uGrayscale;\n\nconst float TAU = 6.283185307179586;\n\nvec2 coverUV(vec2 uv) {\n  vec2 safe = max(uTextureSize, vec2(1.0));\n  vec2 s = uResolution / safe;\n  vec2 scaledSize = safe * max(s.x, s.y);\n  vec2 offset = (uResolution - scaledSize) * 0.5;\n  return (uv * uResolution - offset) / scaledSize;\n}\n\nvoid main() {\n  float amount = texture2D(uDisplacement, vUv).r;\n  vec2 base = coverUV(vUv);\n\n  float theta = amount * uSwirl * TAU;\n  vec2 dir = vec2(sin(theta), cos(theta));\n  vec2 push = dir * amount * uStrength;\n\n  vec3 color;\n  if (uDispersion > 0.001) {\n    float split = uDispersion * 0.25;\n    color.r = texture2D(uTexture, base + push * (1.0 + split)).r;\n    color.g = texture2D(uTexture, base + push).g;\n    color.b = texture2D(uTexture, base + push * (1.0 - split)).b;\n  } else {\n    color = texture2D(uTexture, base + push).rgb;\n  }\n\n  if (uGrayscale > 0.001) {\n    color = mix(color, vec3(dot(color, vec3(0.2126, 0.7152, 0.0722))), uGrayscale);\n  }\n\n  if (uTintAmount > 0.001) {\n    color = mix(color, color * uTint * 1.9, clamp(amount * 1.6, 0.0, 1.0) * uTintAmount);\n  }\n\n  if (uGlint > 0.001) {\n    float ex = texture2D(uDisplacement, vUv + vec2(uTexel.x, 0.0)).r - texture2D(uDisplacement, vUv - vec2(uTexel.x, 0.0)).r;\n    float ey = texture2D(uDisplacement, vUv + vec2(0.0, uTexel.y)).r - texture2D(uDisplacement, vUv - vec2(0.0, uTexel.y)).r;\n    vec3 normal = normalize(vec3(-ex * 26.0, -ey * 26.0, 1.0));\n    vec3 light = normalize(vec3(-0.35, 0.55, 1.0));\n    float raw = pow(max(dot(normal, light), 0.0), 22.0);\n    float flatSpec = pow(max(light.z, 0.0), 22.0);\n    color += uHighlight * clamp((raw - flatSpec) / max(1.0 - flatSpec, 0.0001), 0.0, 1.0) * uGlint;\n  }\n\n  gl_FragColor = vec4(color, 1.0);\n}\n`;\n\nconst hexToRGB = hex => {\n  const clean = hex.replace('#', '');\n  const full =\n    clean.length === 3\n      ? clean\n          .split('')\n          .map(c => c + c)\n          .join('')\n      : clean;\n  const n = parseInt(full, 16);\n  if (Number.isNaN(n)) return [1, 1, 1];\n  return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];\n};\n\nconst RippleDistortion = ({\n  src = 'https://images.unsplash.com/photo-1782977389500-dd7adad33ebe?q=80&w=3416&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',\n  brushSize = 150,\n  strength = 0.2,\n  swirl = 1,\n  rings = 4,\n  spread = 5,\n  fade = 3,\n  spacing = 15,\n  dispersion = 0,\n  glint = 0,\n  tint = '#a855f7',\n  tintAmount = 0.1,\n  grayscale = true,\n  highlightColor = '#ffffff',\n  trigger = 'hover',\n  clickStrength = 2,\n  quality = 'low',\n  enabled = true,\n  className = '',\n  style\n}) => {\n  const mountRef = useRef(null);\n  const configRef = useRef({});\n  const uniformsRef = useRef(null);\n\n  configRef.current = { brushSize, spread, fade, spacing, clickStrength, trigger, enabled };\n\n  useEffect(() => {\n    const mount = mountRef.current;\n    if (!mount) return;\n\n    const reduceMotion =\n      typeof window !== 'undefined' &&\n      window.matchMedia &&\n      window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\n    const renderer = new Renderer({\n      alpha: false,\n      antialias: false,\n      dpr: Math.min(window.devicePixelRatio || 1, 2)\n    });\n    const gl = renderer.gl;\n    gl.clearColor(0, 0, 0, 1);\n    const canvas = gl.canvas;\n    canvas.style.width = '100%';\n    canvas.style.height = '100%';\n    canvas.style.display = 'block';\n    mount.appendChild(canvas);\n\n    const imageTexture = new Texture(gl, {\n      generateMipmaps: false,\n      minFilter: gl.LINEAR,\n      magFilter: gl.LINEAR,\n      wrapS: gl.CLAMP_TO_EDGE,\n      wrapT: gl.CLAMP_TO_EDGE\n    });\n\n    let disposed = false;\n    const image = new window.Image();\n    image.crossOrigin = 'anonymous';\n    image.decoding = 'async';\n    image.onload = () => {\n      if (disposed) return;\n      imageTexture.image = image;\n      compositeUniforms.uTextureSize.value = [image.naturalWidth || 1, image.naturalHeight || 1];\n    };\n    image.src = src;\n\n    const offsets = new Float32Array(MAX_WAVES * 2);\n    const scales = new Float32Array(MAX_WAVES * 2);\n    const opacities = new Float32Array(MAX_WAVES);\n\n    const waves = Array.from({ length: MAX_WAVES }, () => ({\n      x: 0,\n      y: 0,\n      scale: START_SCALE,\n      target: START_SCALE,\n      size: 1,\n      opacity: 0\n    }));\n    let current = 0;\n\n    const geometry = new Geometry(gl, {\n      position: { size: 2, data: new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]) },\n      uv: { size: 2, data: new Float32Array([0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1]) },\n      iOffset: { instanced: 1, size: 2, data: offsets },\n      iScale: { instanced: 1, size: 2, data: scales },\n      iOpacity: { instanced: 1, size: 1, data: opacities }\n    });\n\n    const waveUniforms = { uRings: { value: rings } };\n    const waveProgram = new Program(gl, {\n      vertex: waveVertex,\n      fragment: waveFragment,\n      uniforms: waveUniforms,\n      transparent: true,\n      depthTest: false,\n      depthWrite: false,\n      cullFace: false\n    });\n    waveProgram.setBlendFunc(gl.ONE, gl.ONE);\n    const waveMesh = new Mesh(gl, { geometry, program: waveProgram, frustumCulled: false });\n\n    const displacementTarget = new RenderTarget(gl, {\n      width: 2,\n      height: 2,\n      depth: false,\n      minFilter: gl.LINEAR,\n      magFilter: gl.LINEAR,\n      wrapS: gl.CLAMP_TO_EDGE,\n      wrapT: gl.CLAMP_TO_EDGE\n    });\n\n    const compositeUniforms = {\n      uTexture: { value: imageTexture },\n      uDisplacement: { value: displacementTarget.texture },\n      uResolution: { value: [1, 1] },\n      uTextureSize: { value: [1, 1] },\n      uTexel: { value: [1, 1] },\n      uTint: { value: hexToRGB(tint) },\n      uHighlight: { value: hexToRGB(highlightColor) },\n      uStrength: { value: strength },\n      uSwirl: { value: swirl },\n      uDispersion: { value: dispersion },\n      uGlint: { value: glint },\n      uTintAmount: { value: tintAmount },\n      uGrayscale: { value: grayscale ? 1 : 0 }\n    };\n\n    const compositeMesh = new Mesh(gl, {\n      geometry: new Triangle(gl),\n      program: new Program(gl, {\n        vertex: screenVertex,\n        fragment: compositeFragment,\n        uniforms: compositeUniforms,\n        depthTest: false,\n        depthWrite: false\n      })\n    });\n\n    uniformsRef.current = { wave: waveUniforms, composite: compositeUniforms };\n\n    let width = 1;\n    let height = 1;\n\n    const resize = () => {\n      width = Math.max(1, mount.clientWidth);\n      height = Math.max(1, mount.clientHeight);\n      renderer.setSize(width, height);\n      compositeUniforms.uResolution.value = [width, height];\n\n      const scale = QUALITY_SCALE[quality] || QUALITY_SCALE.high;\n      const fieldW = Math.max(2, Math.round(width * scale));\n      const fieldH = Math.max(2, Math.round(height * scale));\n      displacementTarget.setSize(fieldW, fieldH);\n      compositeUniforms.uTexel.value = [1 / fieldW, 1 / fieldH];\n    };\n\n    const ro = new ResizeObserver(resize);\n    ro.observe(mount);\n    resize();\n\n    const setNewWave = (x, y, power) => {\n      const cfg = configRef.current;\n      const wave = waves[current];\n      current = (current + 1) % MAX_WAVES;\n      wave.x = x;\n      wave.y = y;\n      wave.scale = START_SCALE * power;\n      wave.target = START_SCALE * Math.max(1, cfg.spread) * power;\n      wave.size = Math.max(1, cfg.brushSize);\n      wave.opacity = 1;\n    };\n\n    const localPoint = (clientX, clientY) => {\n      const rect = mount.getBoundingClientRect();\n      if (rect.width === 0 || rect.height === 0) return null;\n      if (clientX < rect.left || clientX > rect.right || clientY < rect.top || clientY > rect.bottom) {\n        return null;\n      }\n      return [clientX - rect.left, rect.height - (clientY - rect.top)];\n    };\n\n    let previousX = 0;\n    let previousY = 0;\n\n    const onMove = event => {\n      const cfg = configRef.current;\n      if (!cfg.enabled || reduceMotion || cfg.trigger === 'click') return;\n      const point = localPoint(event.clientX, event.clientY);\n      if (!point) return;\n      const step = Math.max(1, cfg.spacing);\n      if (Math.abs(point[0] - previousX) > step || Math.abs(point[1] - previousY) > step) {\n        setNewWave(point[0], point[1], 1);\n        previousX = point[0];\n        previousY = point[1];\n      }\n    };\n\n    const onDown = event => {\n      const cfg = configRef.current;\n      if (!cfg.enabled || reduceMotion || cfg.trigger === 'hover') return;\n      const point = localPoint(event.clientX, event.clientY);\n      if (!point) return;\n      setNewWave(point[0], point[1], Math.max(1, cfg.clickStrength));\n    };\n\n    window.addEventListener('pointermove', onMove, { passive: true });\n    window.addEventListener('pointerdown', onDown, { passive: true });\n\n    let raf = 0;\n    let previousTime = 0;\n\n    const loop = now => {\n      raf = requestAnimationFrame(loop);\n      const delta = previousTime ? Math.min(0.05, (now - previousTime) / 1000) : 0;\n      previousTime = now;\n      const cfg = configRef.current;\n\n      const growth = reduceMotion ? 0 : 1 - Math.exp(-delta * 1.09);\n      const decay = reduceMotion ? 1 : Math.exp((-delta * LIFE_CONSTANT) / Math.max(0.15, cfg.fade));\n\n      for (let i = 0; i < MAX_WAVES; i += 1) {\n        const wave = waves[i];\n        if (wave.opacity <= 0) {\n          opacities[i] = 0;\n          continue;\n        }\n\n        wave.opacity *= decay;\n        wave.scale += (wave.target - wave.scale) * growth;\n\n        if (wave.opacity < 0.002) {\n          wave.opacity = 0;\n          opacities[i] = 0;\n          continue;\n        }\n\n        const half = (wave.scale * wave.size) / 2;\n        offsets[i * 2] = (wave.x / width) * 2 - 1;\n        offsets[i * 2 + 1] = (wave.y / height) * 2 - 1;\n        scales[i * 2] = (half / width) * 2;\n        scales[i * 2 + 1] = (half / height) * 2;\n        opacities[i] = wave.opacity;\n      }\n\n      geometry.attributes.iOffset.needsUpdate = true;\n      geometry.attributes.iScale.needsUpdate = true;\n      geometry.attributes.iOpacity.needsUpdate = true;\n\n      renderer.render({ scene: waveMesh, target: displacementTarget, clear: true });\n      renderer.render({ scene: compositeMesh });\n    };\n    raf = requestAnimationFrame(loop);\n\n    return () => {\n      disposed = true;\n      cancelAnimationFrame(raf);\n      ro.disconnect();\n      window.removeEventListener('pointermove', onMove);\n      window.removeEventListener('pointerdown', onDown);\n      uniformsRef.current = null;\n      if (canvas.parentNode === mount) mount.removeChild(canvas);\n      const ext = gl.getExtension('WEBGL_lose_context');\n      if (ext) ext.loseContext();\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [src, quality]);\n\n  useEffect(() => {\n    const u = uniformsRef.current;\n    if (!u) return;\n    u.wave.uRings.value = rings;\n    u.composite.uStrength.value = strength;\n    u.composite.uSwirl.value = swirl;\n    u.composite.uDispersion.value = dispersion;\n    u.composite.uGlint.value = glint;\n    u.composite.uTintAmount.value = tintAmount;\n    u.composite.uGrayscale.value = grayscale ? 1 : 0;\n    u.composite.uHighlight.value = hexToRGB(highlightColor);\n    u.composite.uTint.value = hexToRGB(tint);\n  }, [rings, strength, swirl, dispersion, glint, tintAmount, grayscale, highlightColor, tint]);\n\n  return <div ref={mountRef} className={`ripple-distortion ${className}`.trim()} style={style} />;\n};\n\nexport default RippleDistortion;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"ogl@^1.0.11"
	]
}