{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "PatternWaves-TS-CSS",
	"title": "PatternWaves",
	"description": "A lit halftone surface of dots, lines, crosses or glyphs that moves like draped silk, rolling swells or ripples, with six presets, one-color theming and a cursor that sends ripples through it.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:file",
			"path": "PatternWaves.css",
			"target": "@components/PatternWaves.css",
			"content": ".pattern-waves {\n  position: relative;\n  width: 100%;\n  height: 100%;\n  overflow: hidden;\n}\n"
		},
		{
			"type": "registry:component",
			"path": "PatternWaves.tsx",
			"content": "'use client';\n\nimport { useEffect, useMemo, useRef } from 'react';\nimport type { CSSProperties } from 'react';\nimport { Renderer, Program, Mesh, Triangle, RenderTarget, Texture } from 'ogl';\n\nimport './PatternWaves.css';\n\ntype SurfacePreset = 'silk' | 'ocean' | 'pond' | 'lines' | 'terminal' | 'mesh';\ntype PatternKind = 'dot' | 'square' | 'plus' | 'line' | 'glyph';\ntype WaveKind = 'silk' | 'swell' | 'ripple';\ntype FadeKind = 'edges' | 'center' | 'bottom' | 'top' | 'none';\ntype Rgba = [number, number, number, number];\n\ninterface PresetValues {\n  pattern: PatternKind;\n  wave: WaveKind;\n  spacing: number;\n  markSize: number;\n  depth: number;\n  light: number;\n  shine: number;\n  contrast: number;\n  speed: number;\n  scale: number;\n  direction: number;\n}\n\nexport interface PatternWavesProps extends Partial<PresetValues> {\n  preset?: SurfacePreset;\n  color?: string;\n  backgroundColor?: string;\n  opacity?: number;\n  fade?: FadeKind;\n  fadeSize?: number;\n  characters?: string;\n  interactive?: boolean;\n  cursorSize?: number;\n  cursorStrength?: number;\n  intro?: boolean;\n  paused?: boolean;\n  className?: string;\n  style?: CSSProperties;\n}\n\ninterface Settings extends PresetValues {\n  color: Rgba;\n  background: Rgba;\n  opacity: number;\n  fade: FadeKind;\n  fadeSize: number;\n  characters: string;\n  interactive: boolean;\n  cursorSize: number;\n  cursorStrength: number;\n  intro: boolean;\n  paused: boolean;\n}\n\ninterface RippleField {\n  width: number;\n  height: number;\n  read: RenderTarget;\n  write: RenderTarget;\n}\n\nconst SURFACE_PRESETS: Record<SurfacePreset, PresetValues> = {\n  silk: {\n    pattern: 'dot',\n    wave: 'silk',\n    spacing: 9,\n    markSize: 0.95,\n    depth: 0.95,\n    light: 0,\n    shine: 0.8,\n    contrast: 1.2,\n    speed: 0.35,\n    scale: 1,\n    direction: 20\n  },\n  ocean: {\n    pattern: 'dot',\n    wave: 'swell',\n    spacing: 10,\n    markSize: 0.9,\n    depth: 0.42,\n    light: 0,\n    shine: 1,\n    contrast: 1.2,\n    speed: 0.5,\n    scale: 1,\n    direction: 100\n  },\n  pond: {\n    pattern: 'dot',\n    wave: 'ripple',\n    spacing: 10,\n    markSize: 0.9,\n    depth: 0.55,\n    light: 0,\n    shine: 1.2,\n    contrast: 1.2,\n    speed: 0.5,\n    scale: 1,\n    direction: 35\n  },\n  lines: {\n    pattern: 'line',\n    wave: 'silk',\n    spacing: 12,\n    markSize: 0.42,\n    depth: 0.9,\n    light: 0,\n    shine: 0.6,\n    contrast: 1.2,\n    speed: 0.3,\n    scale: 1,\n    direction: 20\n  },\n  terminal: {\n    pattern: 'glyph',\n    wave: 'ripple',\n    spacing: 13,\n    markSize: 0.95,\n    depth: 0.75,\n    light: 0,\n    shine: 0.9,\n    contrast: 1.4,\n    speed: 0.3,\n    scale: 1.1,\n    direction: 200\n  },\n  mesh: {\n    pattern: 'plus',\n    wave: 'silk',\n    spacing: 14,\n    markSize: 0.8,\n    depth: 0.95,\n    light: 0,\n    shine: 1,\n    contrast: 1.45,\n    speed: 0.4,\n    scale: 1.1,\n    direction: 325\n  }\n};\n\nconst PATTERNS: Record<string, number> = { dot: 0, square: 1, plus: 2, line: 3, glyph: 4 };\nconst WAVES: Record<string, number> = { silk: 0, swell: 1, ripple: 2 };\nconst FADES: Record<string, number> = { none: 0, edges: 1, center: 2, bottom: 3, top: 4 };\nconst DEFAULT_CHARACTERS = '.:-=+*#%@';\nconst GLYPH_FONT = 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace';\nconst ATLAS_TILE = 128;\nconst WAVE_UNIT = 520;\nconst RIPPLE_CELL = 8;\nconst RIPPLE_RATE = 60;\nconst PIXEL_BUDGET = 4.5e6;\nconst INTRO_SECONDS = 2;\n\nconst clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max);\n\nconst luminance = (rgba: number[]) => 0.2126 * rgba[0] + 0.7152 * rgba[1] + 0.0722 * rgba[2];\n\nconst parseColor = (value: string, fallback: Rgba): Rgba => {\n  try {\n    const ctx = document.createElement('canvas').getContext('2d');\n    if (!ctx) return fallback;\n    ctx.fillStyle = '#000000';\n    ctx.fillStyle = value;\n    const resolved = ctx.fillStyle;\n    if (resolved.startsWith('#')) {\n      const n = parseInt(resolved.slice(1), 16);\n      return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255, 1];\n    }\n    const parts = resolved.match(/[\\d.]+/g);\n    if (!parts || parts.length < 3) return fallback;\n    return [Number(parts[0]) / 255, Number(parts[1]) / 255, Number(parts[2]) / 255, parts[3] ? Number(parts[3]) : 1];\n  } catch {\n    return fallback;\n  }\n};\n\nconst buildAtlas = (characters: string) => {\n  const glyphs = Array.from(characters && characters.length ? characters : DEFAULT_CHARACTERS);\n  const columns = Math.ceil(Math.sqrt(glyphs.length));\n  const lines = Math.ceil(glyphs.length / columns);\n  const canvas = document.createElement('canvas');\n  canvas.width = columns * ATLAS_TILE;\n  canvas.height = lines * ATLAS_TILE;\n  const ctx = canvas.getContext('2d');\n  if (!ctx) return null;\n  ctx.fillStyle = '#ffffff';\n  ctx.textAlign = 'center';\n  ctx.textBaseline = 'middle';\n  ctx.font = `500 ${Math.round(ATLAS_TILE * 0.86)}px ${GLYPH_FONT}`;\n  glyphs.forEach((glyph, index) => {\n    const x = (index % columns) * ATLAS_TILE + ATLAS_TILE / 2;\n    const y = Math.floor(index / columns) * ATLAS_TILE + ATLAS_TILE / 2;\n    ctx.fillText(glyph, x, y);\n  });\n  return { canvas, columns, lines, count: glyphs.length };\n};\n\nconst passVertex = `#version 300 es\nin vec2 position;\nvoid main() {\n  gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fieldFragment = `#version 300 es\nprecision highp float;\nprecision highp int;\nuniform vec2 uSize;\nuniform float uDpr;\nuniform vec2 uOrigin;\nuniform vec2 uPitch;\nuniform int uWave;\nuniform float uTime;\nuniform float uUnit;\nuniform vec2 uHeading;\nuniform float uAmp;\nuniform float uDepth;\nuniform vec3 uLight;\nuniform float uShine;\nuniform float uContrast;\nuniform float uInk;\nuniform float uOpacity;\nuniform int uFade;\nuniform float uFadeSize;\nuniform float uAppear;\nuniform sampler2D tRipple;\nuniform float uRipple;\nout vec4 fragColor;\n\nconst float FOLDS = 5.5;\n\nuvec3 scramble(uvec3 v) {\n  v = v * 1664525u + 1013904223u;\n  v.x += v.y * v.z;\n  v.y += v.z * v.x;\n  v.z += v.x * v.y;\n  v ^= v >> 16u;\n  v.x += v.y * v.z;\n  v.y += v.z * v.x;\n  v.z += v.x * v.y;\n  return v;\n}\n\nvec3 lattice(vec3 corner) {\n  uvec3 h = scramble(uvec3(ivec3(corner) + 4096));\n  return vec3(h & 65535u) / 32767.5 - 1.0;\n}\n\nfloat gradientNoise(vec3 p) {\n  vec3 i = floor(p);\n  vec3 f = p - i;\n  vec3 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);\n  float n000 = dot(lattice(i), f);\n  float n100 = dot(lattice(i + vec3(1.0, 0.0, 0.0)), f - vec3(1.0, 0.0, 0.0));\n  float n010 = dot(lattice(i + vec3(0.0, 1.0, 0.0)), f - vec3(0.0, 1.0, 0.0));\n  float n110 = dot(lattice(i + vec3(1.0, 1.0, 0.0)), f - vec3(1.0, 1.0, 0.0));\n  float n001 = dot(lattice(i + vec3(0.0, 0.0, 1.0)), f - vec3(0.0, 0.0, 1.0));\n  float n101 = dot(lattice(i + vec3(1.0, 0.0, 1.0)), f - vec3(1.0, 0.0, 1.0));\n  float n011 = dot(lattice(i + vec3(0.0, 1.0, 1.0)), f - vec3(0.0, 1.0, 1.0));\n  float n111 = dot(lattice(i + vec3(1.0, 1.0, 1.0)), f - vec3(1.0, 1.0, 1.0));\n  return mix(\n    mix(mix(n000, n100, u.x), mix(n010, n110, u.x), u.y),\n    mix(mix(n001, n101, u.x), mix(n011, n111, u.x), u.y),\n    u.z\n  );\n}\n\nvec2 turn(vec2 v, float angle) {\n  float c = cos(angle);\n  float s = sin(angle);\n  return vec2(c * v.x - s * v.y, s * v.x + c * v.y);\n}\n\nfloat surface(vec2 p, float t) {\n  if (uWave == 0) {\n    vec2 side = vec2(-uHeading.y, uHeading.x);\n    float u = dot(p, uHeading);\n    float v = dot(p, side);\n    float bend = gradientNoise(vec3(v * 0.85, u * 0.3, t * 0.05)) * 1.7 + 0.4 * sin(v * 1.6 + t * 0.2);\n    float phase = u * FOLDS + bend - t * 0.45;\n    float swell = 0.6 + 0.4 * gradientNoise(vec3(u * 0.55 + 3.0, v * 0.45, t * 0.04));\n    float fold = sin(phase) + 0.32 * sin(2.0 * phase + 1.3);\n    float ripple = 0.16 * sin(u * FOLDS * 2.5 + bend * 1.9 - t * 0.9 + 2.1);\n    return (fold + ripple) * swell;\n  }\n  if (uWave == 1) {\n    float bend = gradientNoise(vec3(p * 0.6, t * 0.05)) * 0.6;\n    float phase = dot(p, uHeading) * 15.0 + bend * 2.2 - t * 1.4;\n    float swell = sin(phase) + 0.3 * sin(2.0 * phase - 0.8);\n    float roll = 0.75 + 0.25 * gradientNoise(vec3(p * 0.9 + 11.0, t * 0.05));\n    return 0.8 * swell * roll;\n  }\n  float r = length(p + uHeading * 1.2);\n  float bend = gradientNoise(vec3(p * 1.2, t * 0.05)) * 0.1;\n  return sin((r + bend) * 9.0 - t * 1.8) * (0.45 + 0.55 * exp(-(r - 0.6) * 0.8));\n}\n\nfloat heightAt(vec2 css) {\n  float h = surface((css - 0.5 * uSize) / uUnit, uTime) * uAmp;\n  if (uRipple > 0.0) h += texture(tRipple, css / uSize).r * uRipple;\n  return h;\n}\n\nvoid main() {\n  vec2 cell = floor(gl_FragCoord.xy);\n  vec2 center = uOrigin + (cell + 0.5) * uPitch;\n  vec2 css = center / uDpr;\n  vec2 uv = css / uSize;\n  float e = max(uPitch.y / uDpr, 4.0);\n  float h = heightAt(css);\n  float hx = (heightAt(css + vec2(e, 0.0)) - heightAt(css - vec2(e, 0.0))) / (2.0 * e);\n  float hy = (heightAt(css + vec2(0.0, e)) - heightAt(css - vec2(0.0, e))) / (2.0 * e);\n  vec2 grad = vec2(hx, hy) * uUnit * uDepth * 0.4;\n  vec3 n = normalize(vec3(-grad, 1.0));\n\n  float diffuse = clamp(dot(n, uLight), 0.0, 1.0);\n  vec3 halfway = normalize(uLight + vec3(0.0, 0.0, 1.0));\n  float spec = pow(clamp(dot(n, halfway), 0.0, 1.0), 160.0) * uShine * 1.15;\n  float hollow = 0.7 + 0.3 * clamp(h * 0.5 + 0.5, 0.0, 1.0);\n  float tone = clamp(diffuse * hollow * 0.78 + spec, 0.0, 1.0);\n  tone = clamp((tone - 0.42) * uContrast + 0.42, 0.0, 1.0);\n\n  float level = uInk > 0.5 ? pow(clamp(1.0 - tone / 0.46, 0.0, 1.0), 2.4) * 0.72 : pow(tone, 2.2);\n  float emphasis = uInk > 0.5 ? smoothstep(0.55, 0.95, level) : clamp(spec * 1.6, 0.0, 1.0);\n\n  float fade = 1.0;\n  vec2 c = uv * 2.0 - 1.0;\n  if (uFade == 1) {\n    fade = 1.0 - smoothstep(1.0 - uFadeSize, 1.18, length(c));\n  } else if (uFade == 2) {\n    fade = mix(0.05, 1.0, smoothstep(0.08, 0.08 + uFadeSize, length(c * vec2(1.0, 1.35))));\n  } else if (uFade == 3) {\n    fade = smoothstep(0.0, uFadeSize, uv.y);\n  } else if (uFade == 4) {\n    fade = smoothstep(0.0, uFadeSize, 1.0 - uv.y);\n  }\n\n  float reach = length(css - 0.5 * uSize) / max(0.5 * length(uSize), 1.0);\n  float appear = smoothstep(reach - 0.05, reach + 0.3, uAppear * 1.35);\n\n  float alpha = uOpacity * fade * appear * (0.22 + 0.78 * level);\n  float lift = clamp(h * uDepth * 0.42, -0.48, 0.48);\n  fragColor = vec4(level, alpha, emphasis, lift + 0.5);\n}\n`;\n\nconst rippleFragment = `#version 300 es\nprecision highp float;\nuniform sampler2D tState;\nuniform vec2 uTexel;\nuniform vec2 uSize;\nuniform vec2 uFrom;\nuniform vec2 uTo;\nuniform float uRadius;\nuniform float uImpulse;\nuniform float uDamping;\nout vec4 fragColor;\n\nvoid main() {\n  vec2 uv = gl_FragCoord.xy * uTexel;\n  vec2 state = texture(tState, uv).rg;\n  float left = texture(tState, uv - vec2(uTexel.x, 0.0)).r;\n  float right = texture(tState, uv + vec2(uTexel.x, 0.0)).r;\n  float below = texture(tState, uv - vec2(0.0, uTexel.y)).r;\n  float above = texture(tState, uv + vec2(0.0, uTexel.y)).r;\n  float next = ((left + right + below + above) * 0.5 - state.g) * uDamping;\n  vec2 p = uv * uSize;\n  vec2 segment = uTo - uFrom;\n  float along = clamp(dot(p - uFrom, segment) / max(dot(segment, segment), 1e-4), 0.0, 1.0);\n  float d = length(p - uFrom - segment * along) / uRadius;\n  next -= uImpulse * exp(-d * d * 2.0);\n  fragColor = vec4(next, state.r, 0.0, 1.0);\n}\n`;\n\nconst markFragment = `#version 300 es\nprecision highp float;\nprecision highp int;\nuniform sampler2D tField;\nuniform sampler2D tAtlas;\nuniform vec2 uOrigin;\nuniform vec2 uPitch;\nuniform vec2 uGrid;\nuniform int uPattern;\nuniform float uMarkSize;\nuniform float uStroke;\nuniform vec3 uColor;\nuniform vec3 uAccent;\nuniform vec4 uBackground;\nuniform vec3 uAtlas;\nout vec4 fragColor;\n\nfloat box(vec2 p, vec2 b, float r) {\n  vec2 q = abs(p) - b + r;\n  return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;\n}\n\nfloat coverage(vec2 local, float level) {\n  float span = uPitch.y * uMarkSize;\n  float area = max(sqrt(level), 0.14);\n  if (uPattern == 0) {\n    return clamp(0.5 - (length(local) - 0.5 * span * area), 0.0, 1.0);\n  }\n  if (uPattern == 1) {\n    float extent = 0.5 * span * area;\n    return clamp(0.5 - box(local, vec2(extent), extent * 0.3), 0.0, 1.0);\n  }\n  if (uPattern == 2) {\n    float arm = 0.5 * span * mix(0.3, 1.0, level);\n    float width = 0.5 * uStroke;\n    return clamp(0.5 - min(box(local, vec2(arm, width), width), box(local, vec2(width, arm), width)), 0.0, 1.0);\n  }\n  if (uAtlas.z < 1.0) return 0.0;\n  vec2 g = local / span + 0.5;\n  if (g.x < 0.0 || g.y < 0.0 || g.x > 1.0 || g.y > 1.0) return 0.0;\n  float index = min(floor(level * uAtlas.z), uAtlas.z - 1.0);\n  vec2 tile = vec2(mod(index, uAtlas.x), floor(index / uAtlas.x));\n  vec2 atlasUv = (tile + vec2(g.x, 1.0 - g.y)) / uAtlas.xy;\n  vec2 texel = 1.0 / (span * uAtlas.xy);\n  return textureGrad(tAtlas, atlasUv, vec2(texel.x, 0.0), vec2(0.0, texel.y)).a;\n}\n\nvec4 lineInk(vec2 rel) {\n  float fx = rel.x / uPitch.x - 0.5;\n  float c0 = clamp(floor(fx), 0.0, uGrid.x - 1.0);\n  float c1 = min(c0 + 1.0, uGrid.x - 1.0);\n  float t = clamp(fx - c0, 0.0, 1.0);\n  float row = floor(rel.y / uPitch.y);\n  vec4 ink = vec4(0.0);\n  for (int k = -1; k <= 1; k++) {\n    float cy = row + float(k);\n    if (cy < 0.0 || cy >= uGrid.y) continue;\n    vec4 a = texelFetch(tField, ivec2(int(c0), int(cy)), 0);\n    vec4 b = texelFetch(tField, ivec2(int(c1), int(cy)), 0);\n    vec4 f = mix(a, b, t);\n    if (f.g < 0.002) continue;\n    float y = (cy + 0.5 + f.a - 0.5) * uPitch.y;\n    float slope = (b.a - a.a) * uPitch.y / uPitch.x;\n    float thickness = max(uStroke, uPitch.y * uMarkSize * f.r);\n    float d = abs(rel.y - y) / sqrt(1.0 + slope * slope) - 0.5 * thickness;\n    float alpha = clamp(0.5 - d, 0.0, 1.0) * f.g;\n    if (alpha > ink.a) ink = vec4(mix(uColor, uAccent, f.b) * alpha, alpha);\n  }\n  return ink;\n}\n\nvoid main() {\n  vec2 rel = gl_FragCoord.xy - uOrigin;\n  vec4 background = vec4(uBackground.rgb * uBackground.a, uBackground.a);\n  vec4 ink = vec4(0.0);\n  if (uPattern == 3) {\n    ink = lineInk(rel);\n  } else {\n    float cx = floor(rel.x / uPitch.x);\n    float row = floor(rel.y / uPitch.y);\n    if (cx >= 0.0 && cx < uGrid.x) {\n      for (int k = -1; k <= 1; k++) {\n        float cy = row + float(k);\n        if (cy < 0.0 || cy >= uGrid.y) continue;\n        vec4 f = texelFetch(tField, ivec2(int(cx), int(cy)), 0);\n        if (f.g < 0.002) continue;\n        vec2 center = (vec2(cx, cy) + 0.5) * uPitch + vec2(0.0, (f.a - 0.5) * uPitch.y);\n        float alpha = coverage(rel - center, f.r) * f.g;\n        if (alpha > ink.a) ink = vec4(mix(uColor, uAccent, f.b) * alpha, alpha);\n      }\n    }\n  }\n  fragColor = ink + background * (1.0 - ink.a);\n}\n`;\n\nconst PatternWaves = ({\n  preset = 'silk',\n  pattern,\n  wave,\n  spacing,\n  markSize,\n  depth,\n  light,\n  shine,\n  contrast,\n  speed,\n  scale,\n  direction,\n  color = '#ffffff',\n  backgroundColor = '#000000',\n  opacity = 1,\n  fade = 'edges',\n  fadeSize = 0.5,\n  characters = DEFAULT_CHARACTERS,\n  interactive = true,\n  cursorSize = 50,\n  cursorStrength = 0.6,\n  intro = true,\n  paused = false,\n  className = '',\n  style\n}: PatternWavesProps) => {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const settingsRef = useRef<Settings | null>(null);\n  const wakeRef = useRef<(() => void) | null>(null);\n\n  const base = SURFACE_PRESETS[preset] || SURFACE_PRESETS.silk;\n  const pick = <K extends keyof PresetValues>(value: PresetValues[K] | undefined, key: K): PresetValues[K] =>\n    value === undefined || value === null ? base[key] : value;\n\n  const colors = useMemo(\n    () => ({\n      color: parseColor(color, [1, 1, 1, 1]),\n      background: parseColor(backgroundColor, [0, 0, 0, 1])\n    }),\n    [color, backgroundColor]\n  );\n\n  useEffect(() => {\n    settingsRef.current = {\n      ...colors,\n      pattern: pick(pattern, 'pattern'),\n      wave: pick(wave, 'wave'),\n      spacing: pick(spacing, 'spacing'),\n      markSize: pick(markSize, 'markSize'),\n      depth: pick(depth, 'depth'),\n      light: pick(light, 'light'),\n      shine: pick(shine, 'shine'),\n      contrast: pick(contrast, 'contrast'),\n      speed: pick(speed, 'speed'),\n      scale: pick(scale, 'scale'),\n      direction: pick(direction, 'direction'),\n      opacity,\n      fade,\n      fadeSize,\n      characters,\n      interactive,\n      cursorSize,\n      cursorStrength,\n      intro,\n      paused\n    };\n    wakeRef.current?.();\n  });\n\n  useEffect(() => {\n    const container = containerRef.current;\n    if (!container) return undefined;\n\n    const renderer = new Renderer({ alpha: true, premultipliedAlpha: true, antialias: false, depth: false });\n    const gl = renderer.gl;\n    if (!renderer.isWebgl2) {\n      gl.getExtension('WEBGL_lose_context')?.loseContext();\n      return undefined;\n    }\n    gl.clearColor(0, 0, 0, 0);\n    const canvas = gl.canvas;\n    canvas.style.display = 'block';\n    canvas.style.width = '100%';\n    canvas.style.height = '100%';\n    canvas.setAttribute('aria-hidden', 'true');\n    container.appendChild(canvas);\n\n    const geometry = new Triangle(gl);\n    const blank = new Texture(gl);\n    const floatTargets = !!gl.getExtension('EXT_color_buffer_float');\n\n    const dispose = (target: RenderTarget) => {\n      gl.deleteFramebuffer(target.buffer);\n      gl.deleteTexture(target.texture.texture);\n    };\n\n    const fieldTargetFor = (w: number, h: number) =>\n      new RenderTarget(gl, { width: w, height: h, depth: false, minFilter: gl.NEAREST, magFilter: gl.NEAREST });\n\n    const rippleTargetFor = (w: number, h: number) =>\n      new RenderTarget(gl, {\n        width: w,\n        height: h,\n        depth: false,\n        type: (gl as unknown as WebGL2RenderingContext).HALF_FLOAT,\n        format: gl.RGBA,\n        internalFormat: (gl as unknown as WebGL2RenderingContext).RGBA16F,\n        minFilter: gl.LINEAR,\n        magFilter: gl.LINEAR\n      });\n\n    let fieldTarget = fieldTargetFor(1, 1);\n    let ripple: RippleField | null = null;\n\n    const atlasTexture = new Texture(gl, {\n      generateMipmaps: true,\n      minFilter: gl.LINEAR_MIPMAP_LINEAR,\n      magFilter: gl.LINEAR,\n      flipY: false\n    });\n\n    const fieldUniforms = {\n      uSize: { value: [1, 1] },\n      uDpr: { value: 1 },\n      uOrigin: { value: [0, 0] },\n      uPitch: { value: [1, 1] },\n      uWave: { value: 0 },\n      uTime: { value: 0 },\n      uUnit: { value: WAVE_UNIT },\n      uHeading: { value: [1, 0] },\n      uAmp: { value: 0 },\n      uDepth: { value: 0.5 },\n      uLight: { value: [0, 0, 1] },\n      uShine: { value: 0.8 },\n      uContrast: { value: 1 },\n      uInk: { value: 0 },\n      uOpacity: { value: 1 },\n      uFade: { value: 0 },\n      uFadeSize: { value: 0.5 },\n      uAppear: { value: 0 },\n      tRipple: { value: blank },\n      uRipple: { value: 0 }\n    };\n    const fieldMesh = new Mesh(gl, {\n      geometry,\n      program: new Program(gl, {\n        vertex: passVertex,\n        fragment: fieldFragment,\n        uniforms: fieldUniforms,\n        depthTest: false,\n        depthWrite: false\n      })\n    });\n\n    const rippleUniforms = {\n      tState: { value: blank },\n      uTexel: { value: [1, 1] },\n      uSize: { value: [1, 1] },\n      uFrom: { value: [0, 0] },\n      uTo: { value: [0, 0] },\n      uRadius: { value: 40 },\n      uImpulse: { value: 0 },\n      uDamping: { value: 0.975 }\n    };\n    const rippleMesh = new Mesh(gl, {\n      geometry,\n      program: new Program(gl, {\n        vertex: passVertex,\n        fragment: rippleFragment,\n        uniforms: rippleUniforms,\n        depthTest: false,\n        depthWrite: false\n      })\n    });\n\n    const markUniforms = {\n      tField: { value: fieldTarget.texture },\n      tAtlas: { value: atlasTexture },\n      uOrigin: { value: [0, 0] },\n      uPitch: { value: [1, 1] },\n      uGrid: { value: [1, 1] },\n      uPattern: { value: 0 },\n      uMarkSize: { value: 0.9 },\n      uStroke: { value: 2 },\n      uColor: { value: [1, 1, 1] },\n      uAccent: { value: [1, 1, 1] },\n      uBackground: { value: [0, 0, 0, 1] },\n      uAtlas: { value: [1, 1, 0] }\n    };\n    const markMesh = new Mesh(gl, {\n      geometry,\n      program: new Program(gl, {\n        vertex: passVertex,\n        fragment: markFragment,\n        uniforms: markUniforms,\n        depthTest: false,\n        depthWrite: false\n      })\n    });\n\n    const reducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;\n\n    let width = 1;\n    let height = 1;\n    let raf = 0;\n    let last = performance.now();\n    let time = 0;\n    let introClock = 0;\n    let visible = true;\n    let alive = true;\n    let dirty = true;\n    let rippleUntil = 0;\n    let rippleLive = false;\n    let rippleClock = 0;\n    let atlasKey = '';\n    const pointer = { x: 0, y: 0, inside: false, placed: false, lastX: 0, lastY: 0, burst: 0 };\n\n    const buildRipple = () => {\n      if (!floatTargets) return;\n      const w = clamp(Math.ceil(width / RIPPLE_CELL), 4, 512);\n      const h = clamp(Math.ceil(height / RIPPLE_CELL), 4, 512);\n      if (ripple && ripple.width === w && ripple.height === h) return;\n      if (ripple) {\n        dispose(ripple.read);\n        dispose(ripple.write);\n      }\n      ripple = { width: w, height: h, read: rippleTargetFor(w, h), write: rippleTargetFor(w, h) };\n      rippleLive = false;\n    };\n\n    const clearRipple = () => {\n      if (!ripple) return;\n      [ripple.read, ripple.write].forEach(target => {\n        renderer.bindFramebuffer(target);\n        gl.viewport(0, 0, target.width, target.height);\n        gl.clear(gl.COLOR_BUFFER_BIT);\n      });\n      renderer.bindFramebuffer();\n    };\n\n    const refreshAtlas = (s: Settings) => {\n      if (s.characters === atlasKey) return;\n      atlasKey = s.characters;\n      const built = buildAtlas(s.characters);\n      if (!built) return;\n      atlasTexture.image = built.canvas;\n      markUniforms.uAtlas.value = [built.columns, built.lines, built.count];\n    };\n\n    const resize = () => {\n      width = Math.max(1, container.clientWidth);\n      height = Math.max(1, container.clientHeight);\n      renderer.dpr = Math.min(window.devicePixelRatio || 1, 2, Math.sqrt(PIXEL_BUDGET / (width * height)));\n      renderer.setSize(width, height);\n      buildRipple();\n      dirty = true;\n      start();\n    };\n\n    const stepRipple = (field: RippleField, s: Settings, dt: number) => {\n      rippleClock = Math.min(rippleClock + dt * RIPPLE_RATE, 4);\n      const strength = clamp(s.cursorStrength, 0, 1);\n      const moved = Math.hypot(pointer.x - pointer.lastX, pointer.y - pointer.lastY);\n      let impulse = (pointer.inside ? Math.min(moved / 14, 1) * 0.35 * strength : 0) + pointer.burst * strength;\n      pointer.burst = 0;\n      rippleUniforms.uTexel.value = [1 / field.width, 1 / field.height];\n      rippleUniforms.uSize.value = [width, height];\n      rippleUniforms.uFrom.value = [pointer.lastX, height - pointer.lastY];\n      rippleUniforms.uTo.value = [pointer.x, height - pointer.y];\n      rippleUniforms.uRadius.value = clamp(s.cursorSize, 8, 400);\n      while (rippleClock >= 1) {\n        rippleClock -= 1;\n        rippleUniforms.tState.value = field.read.texture;\n        rippleUniforms.uImpulse.value = impulse;\n        renderer.render({ scene: rippleMesh, target: field.write, clear: false });\n        const next = field.read;\n        field.read = field.write;\n        field.write = next;\n        impulse = 0;\n      }\n      pointer.lastX = pointer.x;\n      pointer.lastY = pointer.y;\n    };\n\n    const frame = (now: number) => {\n      raf = 0;\n      if (!alive) return;\n      const s = settingsRef.current;\n      const dt = Math.min(0.05, Math.max(1 / 240, (now - last) / 1000));\n      last = now;\n      if (!s) return;\n\n      const moving = !s.paused && !reducedMotion && s.speed !== 0;\n      if (moving) time += dt * s.speed;\n      introClock = s.intro && !reducedMotion ? Math.min(1, introClock + dt / INTRO_SECONDS) : 1;\n      const appear = 1 - Math.pow(1 - clamp(introClock / 0.75, 0, 1), 3);\n      const rise = clamp((introClock - 0.1) / 0.9, 0, 1);\n      const amp = rise * rise * (3 - 2 * rise);\n\n      const rippling = s.interactive && !reducedMotion && !!ripple && s.cursorStrength > 0;\n      if (rippling && pointer.inside && !pointer.placed) {\n        pointer.lastX = pointer.x;\n        pointer.lastY = pointer.y;\n        pointer.placed = true;\n      }\n      if (rippling && (pointer.inside || pointer.burst > 0)) rippleUntil = now + 5000;\n      const rippleActive = rippling && now < rippleUntil;\n      if (rippleActive && ripple) {\n        stepRipple(ripple, s, dt);\n        rippleLive = true;\n      } else if (rippleLive) {\n        clearRipple();\n        rippleLive = false;\n      }\n\n      const patternIndex = PATTERNS[s.pattern] ?? 0;\n      if (patternIndex === 4) refreshAtlas(s);\n\n      const canvasW = gl.canvas.width;\n      const canvasH = gl.canvas.height;\n      const dpr = canvasW / width;\n      const pitchY = Math.max(4, Math.round(clamp(s.spacing, 4, 120) * dpr));\n      const pitchX = patternIndex === 3 ? Math.max(2, Math.round(pitchY / 4)) : pitchY;\n      const cols = Math.min(4096, Math.ceil(canvasW / pitchX) + 1);\n      const rows = Math.min(4096, Math.ceil(canvasH / pitchY) + 2);\n      const origin = [Math.floor((canvasW - cols * pitchX) / 2), Math.floor((canvasH - rows * pitchY) / 2)];\n      if (fieldTarget.width !== cols || fieldTarget.height !== rows) {\n        dispose(fieldTarget);\n        fieldTarget = fieldTargetFor(cols, rows);\n        markUniforms.tField.value = fieldTarget.texture;\n      }\n      const heading = (s.direction * Math.PI) / 180;\n      const lightAngle = ((s.direction + 180 + clamp(s.light, -90, 90)) * Math.PI) / 180;\n      const background = s.background;\n      const ink = background[3] > 0.02 ? luminance(s.color) < luminance(background) : luminance(s.color) < 0.5;\n\n      fieldUniforms.uSize.value = [width, height];\n      fieldUniforms.uDpr.value = dpr;\n      fieldUniforms.uOrigin.value = origin;\n      fieldUniforms.uPitch.value = [pitchX, pitchY];\n      fieldUniforms.uWave.value = WAVES[s.wave] ?? 0;\n      fieldUniforms.uTime.value = time;\n      fieldUniforms.uUnit.value = WAVE_UNIT * clamp(s.scale, 0.2, 5);\n      fieldUniforms.uHeading.value = [Math.cos(heading), Math.sin(heading)];\n      fieldUniforms.uAmp.value = amp;\n      fieldUniforms.uDepth.value = clamp(s.depth, 0, 1.5);\n      fieldUniforms.uLight.value = [Math.cos(lightAngle) * 0.78, Math.sin(lightAngle) * 0.78, 0.62];\n      fieldUniforms.uShine.value = clamp(s.shine, 0, 2);\n      fieldUniforms.uContrast.value = clamp(s.contrast, 0.3, 3);\n      fieldUniforms.uInk.value = ink ? 1 : 0;\n      fieldUniforms.uOpacity.value = clamp(s.opacity, 0, 1);\n      fieldUniforms.uFade.value = FADES[s.fade] ?? 0;\n      fieldUniforms.uFadeSize.value = clamp(s.fadeSize, 0.05, 1);\n      fieldUniforms.uAppear.value = appear;\n      fieldUniforms.tRipple.value = ripple && rippleLive ? ripple.read.texture : blank;\n      fieldUniforms.uRipple.value = rippleLive ? 0.32 : 0;\n      renderer.render({ scene: fieldMesh, target: fieldTarget });\n\n      markUniforms.uOrigin.value = origin;\n      markUniforms.uPitch.value = [pitchX, pitchY];\n      markUniforms.uGrid.value = [cols, rows];\n      markUniforms.uPattern.value = patternIndex;\n      markUniforms.uMarkSize.value = clamp(s.markSize, 0.05, 1);\n      markUniforms.uStroke.value = patternIndex === 3 ? 0.9 * dpr : Math.max(1.1 * dpr, pitchY * 0.08);\n      const tint = ink ? 0 : 1;\n      const blend = ink ? 0.35 : 0.55;\n      markUniforms.uColor.value = s.color.slice(0, 3);\n      markUniforms.uAccent.value = s.color.slice(0, 3).map(channel => channel + (tint - channel) * blend);\n      markUniforms.uBackground.value = background;\n      renderer.render({ scene: markMesh });\n      dirty = false;\n\n      if (visible && (moving || introClock < 1 || rippleActive || dirty)) raf = requestAnimationFrame(frame);\n    };\n\n    const start = () => {\n      if (raf || !visible || !alive) return;\n      last = performance.now();\n      raf = requestAnimationFrame(frame);\n    };\n\n    const locate = (e: PointerEvent) => {\n      const rect = container.getBoundingClientRect();\n      const x = e.clientX - rect.left;\n      const y = e.clientY - rect.top;\n      return { x, y, inside: x >= 0 && y >= 0 && x <= rect.width && y <= rect.height };\n    };\n\n    const onPointerMove = (e: PointerEvent) => {\n      const spot = locate(e);\n      pointer.x = spot.x;\n      pointer.y = spot.y;\n      if (spot.inside !== pointer.inside) pointer.placed = false;\n      pointer.inside = spot.inside;\n      if (spot.inside) start();\n    };\n\n    const onPointerDown = (e: PointerEvent) => {\n      const spot = locate(e);\n      if (!spot.inside) return;\n      pointer.x = spot.x;\n      pointer.y = spot.y;\n      if (!pointer.inside) {\n        pointer.lastX = spot.x;\n        pointer.lastY = spot.y;\n      }\n      pointer.burst = 1.2;\n      start();\n    };\n\n    const onPointerLeave = () => {\n      pointer.inside = false;\n      pointer.placed = false;\n      start();\n    };\n\n    const onPointerOut = (e: PointerEvent) => {\n      if (!e.relatedTarget) onPointerLeave();\n    };\n\n    const onPointerUp = (e: PointerEvent) => {\n      if (e.pointerType === 'touch') onPointerLeave();\n    };\n\n    const onVisibility = () => {\n      if (!document.hidden) start();\n    };\n\n    window.addEventListener('pointermove', onPointerMove, { passive: true });\n    window.addEventListener('pointerdown', onPointerDown, { passive: true });\n    window.addEventListener('pointerout', onPointerOut, { passive: true });\n    window.addEventListener('pointerup', onPointerUp, { passive: true });\n    window.addEventListener('blur', onPointerLeave);\n    document.addEventListener('visibilitychange', onVisibility);\n\n    const resizeObserver = new ResizeObserver(resize);\n    resizeObserver.observe(container);\n    const intersectionObserver = new IntersectionObserver(([entry]) => {\n      visible = entry.isIntersecting;\n      start();\n    });\n    intersectionObserver.observe(container);\n\n    wakeRef.current = () => {\n      dirty = true;\n      start();\n    };\n    resize();\n\n    return () => {\n      alive = false;\n      visible = false;\n      cancelAnimationFrame(raf);\n      resizeObserver.disconnect();\n      intersectionObserver.disconnect();\n      window.removeEventListener('pointermove', onPointerMove);\n      window.removeEventListener('pointerdown', onPointerDown);\n      window.removeEventListener('pointerout', onPointerOut);\n      window.removeEventListener('pointerup', onPointerUp);\n      window.removeEventListener('blur', onPointerLeave);\n      document.removeEventListener('visibilitychange', onVisibility);\n      wakeRef.current = null;\n      if (ripple) {\n        dispose(ripple.read);\n        dispose(ripple.write);\n      }\n      dispose(fieldTarget);\n      gl.deleteTexture(atlasTexture.texture);\n      gl.getExtension('WEBGL_lose_context')?.loseContext();\n      if (canvas.parentNode) canvas.parentNode.removeChild(canvas);\n    };\n  }, []);\n\n  return <div ref={containerRef} className={`pattern-waves ${className}`.trim()} style={style} />;\n};\n\nexport default PatternWaves;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"ogl@^1.0.11"
	]
}