{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "GradientASCII-JS-TW",
	"title": "GradientASCII",
	"description": "Soft gradient glows drawn in ASCII characters that scramble under the cursor.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "GradientASCII/GradientASCII.jsx",
			"content": "'use client';\n\nimport { useEffect, useRef } from 'react';\n\nconst MAX_COLORS = 8;\nconst MAX_CLICKS = 4;\nconst MAX_TRAIL = 24;\nconst CLICK_LIFE = 1.2;\nconst TRAIL_LIFE = 0.7;\nconst TRAIL_STEP = 0.015;\nconst INTRO_SECONDS = 2.4;\nconst ATLAS_CELL_W = 60;\nconst ATLAS_CELL_H = 100;\nconst ATLAS_COLUMNS = 16;\nconst FONT_SCALE = 0.78;\nconst DEFAULT_COLORS = ['#1e3fd8', '#f6f8ff', '#a5d8ff', '#38bdf8'];\nconst DEFAULT_CHARACTERS = ' =+xX0S8#@';\nconst DEFAULT_FONT = '\"Geist Mono\", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace';\nconst SHAPES = { horizon: 0, pinch: 1, rise: 2, eclipse: 3 };\nconst CLICK_EFFECTS = { none: 0, wave: 1, burst: 2 };\n\nconst VERTEX = `#version 300 es\nout vec2 vUv;\nvoid main() {\n  vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));\n  vUv = p;\n  gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);\n}`;\n\nconst COMMON = `\nfloat hash12(vec2 p) {\n  vec3 q = fract(vec3(p.xyx) * 0.1031);\n  q += dot(q, q.yzx + 33.33);\n  return fract((q.x + q.y) * q.z);\n}\n`;\n\nconst FIELD = `#version 300 es\nprecision highp float;\nprecision highp int;\nuniform float uAspect;\nuniform float uTime;\nuniform int uShape;\nuniform vec3 uRamp[8];\nuniform int uCount;\nuniform float uScale;\nuniform float uSoftness;\nuniform float uFlow;\nuniform vec2 uSeed;\nuniform vec4 uTrail[24];\nuniform float uRadius;\nuniform vec4 uClicks[4];\nuniform int uClickEffect;\nuniform float uIntro;\nuniform float uFrame;\nin vec2 vUv;\nlayout(location = 0) out vec4 outColor;\nlayout(location = 1) out vec4 outAux;\n${COMMON}\nvec2 hash22(vec2 p) {\n  vec3 q = fract(vec3(p.xyx) * vec3(0.1031, 0.103, 0.0973));\n  q += dot(q, q.yzx + 33.33);\n  return fract((q.xx + q.yz) * q.zy) * 2.0 - 1.0;\n}\n\nfloat noise(vec2 p) {\n  vec2 i = floor(p);\n  vec2 f = fract(p);\n  vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);\n  float a = dot(hash22(i), f);\n  float b = dot(hash22(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0));\n  float c = dot(hash22(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0));\n  float d = dot(hash22(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0));\n  return 0.5 + 0.5 * mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nvec3 labToRgb(vec3 lab) {\n  float l = lab.x + 0.3963377774 * lab.y + 0.2158037573 * lab.z;\n  float m = lab.x - 0.1055613458 * lab.y - 0.0638541728 * lab.z;\n  float s = lab.x - 0.0894841775 * lab.y - 1.2914855480 * lab.z;\n  l = l * l * l;\n  m = m * m * m;\n  s = s * s * s;\n  return vec3(\n    4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,\n    -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,\n    -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s\n  );\n}\n\nvec3 encode(vec3 c) {\n  c = clamp(c, 0.0, 1.0);\n  return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));\n}\n\nvec3 horizon(vec2 p, float time, out float order) {\n  float span = 0.5 * max(uAspect, 1.0);\n  float x = p.x / span;\n  float arch = -0.17 - 0.2 * x * x + 0.02 * sin(time * 0.6);\n  float soft = mix(0.05, 0.22, uSoftness);\n  float edge = p.y - arch;\n  float rim = 0.38 * smoothstep(-soft, 0.42, edge);\n  vec2 sun = vec2(0.08 * sin(time * 0.31) * span, 0.58);\n  float spread = length((p - sun) / vec2(0.95 * span, 0.75));\n  float sky = mix(0.55, 1.0, smoothstep(0.2, 0.95, spread));\n  order = clamp(edge * 1.3, 0.0, 1.0);\n  return vec3(rim, sky, smoothstep(0.18, 0.62, edge));\n}\n\nvec3 pinch(vec2 p, float time, out float order) {\n  float span = 0.5 * max(uAspect, 0.75);\n  float x = p.x / span;\n  float sway = 0.02 * sin(x * 1.7 + time * 0.55);\n  float breathe = 1.0 + 0.06 * sin(time * 0.8);\n  float width = (0.13 + 0.22 * x * x) * breathe * mix(0.85, 1.2, uSoftness);\n  float y = abs(p.y - sway) / width;\n  float t = exp(-pow(y, 2.2)) * (0.42 + 0.58 * exp(-x * x * 1.8));\n  order = clamp(abs(p.y - sway) * 2.4 + abs(x) * 0.3, 0.0, 1.0);\n  return vec3(t, t, 0.0);\n}\n\nvec3 rise(vec2 p, float time, out float order) {\n  float wide = max(uAspect, 0.9);\n  vec2 center = vec2(0.05 * sin(time * 0.45) * wide, -0.7);\n  vec2 radius = vec2(0.82 * wide, 0.64 * (1.0 + 0.04 * sin(time * 0.8)));\n  float d = length((p - center) / radius);\n  float soft = mix(0.6, 1.15, uSoftness);\n  float t = clamp(pow(clamp(1.0 - d, 0.0, 1.0), soft) * 1.35, 0.0, 1.0);\n  order = clamp((d - 0.2) * 1.3, 0.0, 1.0);\n  return vec3(t, t, 0.0);\n}\n\nvec3 eclipse(vec2 p, float time, out float order) {\n  float fit = min(1.0, uAspect * 1.25);\n  float r = 0.23 * fit * (1.0 + 0.015 * sin(time * 0.9));\n  float angle = atan(p.y, p.x);\n  float rays = noise(vec2(cos(angle), sin(angle)) * 2.2 + time * 0.08) * 0.8 + 0.6;\n  float d = length(p) - r;\n  float reach = mix(0.07, 0.15, uSoftness) * rays * fit;\n  float t = smoothstep(-0.006, 0.006 + 0.02 * uSoftness, d) * exp(-max(d, 0.0) / reach);\n  order = clamp(d * 2.0, 0.0, 1.0);\n  return vec3(t, t, 0.0);\n}\n\nvec4 tone(float t) {\n  float x = smoothstep(0.3, 1.0, t) * float(uCount - 1);\n  int index = int(floor(x));\n  return vec4(mix(uRamp[index], uRamp[min(index + 1, uCount - 1)], fract(x)), smoothstep(0.0, 0.38, t));\n}\n\nvoid main() {\n  vec2 p = (vUv - 0.5) * vec2(uAspect, 1.0);\n  float time = uTime;\n  vec2 q = p / uScale;\n  vec2 warp = vec2(\n    noise(q * 1.6 + uSeed + vec2(0.0, time * 0.3)),\n    noise(q * 1.6 + uSeed.yx + vec2(4.3, -time * 0.24))\n  ) - 0.5;\n  q += warp * uFlow * (uShape == 3 ? 0.04 : 0.18);\n  float order = 0.0;\n  vec3 shape;\n  if (uShape == 0) shape = horizon(q, time, order);\n  else if (uShape == 1) shape = pinch(q, time, order);\n  else if (uShape == 2) shape = rise(q, time, order);\n  else shape = eclipse(q, time, order);\n\n  float reveal = clamp((uIntro * 1.45 - order) / 0.45, 0.0, 1.0);\n  reveal = reveal * reveal * (3.0 - 2.0 * reveal);\n\n  float scramble = 0.0;\n  for (int i = 0; i < 24; i++) {\n    vec4 point = uTrail[i];\n    if (point.z <= 0.0) continue;\n    vec2 d = p - point.xy;\n    scramble = max(scramble, point.z * exp(-dot(d, d) / (uRadius * uRadius)));\n  }\n  for (int i = 0; i < 4; i++) {\n    vec4 c = uClicks[i];\n    if (c.w <= 0.0) continue;\n    float d = length(p - c.xy);\n    if (uClickEffect == 1) {\n      scramble = max(scramble, exp(-pow((d - c.z * 0.75) / 0.06, 2.0)) * c.w);\n    } else if (uClickEffect == 2) {\n      float size = 0.08 + 0.22 * (1.0 - exp(-c.z * 5.0));\n      scramble = max(scramble, (1.0 - smoothstep(size * 0.6, size, d)) * c.w);\n    }\n  }\n  vec2 t = clamp(shape.xy * mix(0.7, 1.0, reveal), 0.0, 1.0);\n  vec4 mixed = mix(tone(t.x), tone(t.y), shape.z);\n  vec3 color = encode(labToRgb(mixed.rgb));\n  float alpha = mixed.a * reveal;\n  float dither = (hash12(gl_FragCoord.xy + uFrame * 13.7) - 0.5) / 255.0;\n  outColor = clamp(vec4(color * alpha + dither, alpha + dither), 0.0, 1.0);\n  outAux = vec4(reveal, scramble, 0.0, 1.0);\n}`;\n\nconst GLYPHS = `\nuniform sampler2D uField;\nuniform sampler2D uAux;\nuniform sampler2D uAtlas;\nuniform vec2 uResolution;\nuniform vec2 uCell;\nuniform vec2 uOffset;\nuniform vec2 uAtlasGrid;\nuniform float uCount;\nuniform float uDensity;\nuniform float uOpacity;\nuniform float uFlicker;\nuniform float uDepth;\nuniform float uGlyphTime;\nuniform float uSeedValue;\nuniform float uLight;\n${COMMON}\nfloat sampleGlyph(float index, vec2 local, vec2 coord, float soft) {\n  float column = mod(index, uAtlasGrid.x);\n  float row = floor(index / uAtlasGrid.x);\n  vec2 atlasUv = vec2((column + local.x) / uAtlasGrid.x, (row + 1.0 - local.y) / uAtlasGrid.y);\n  vec2 continuous = coord / uAtlasGrid;\n  float scale = exp2(soft);\n  return textureGrad(uAtlas, atlasUv, dFdx(continuous) * scale, dFdy(continuous) * scale).a;\n}\n\nvec4 glyphAt(vec2 px) {\n  vec2 coord = (px - uOffset) / uCell;\n  vec2 cell = floor(coord);\n  vec2 local = coord - cell;\n  vec2 uv = clamp(((cell + 0.5) * uCell + uOffset) / uResolution, vec2(0.0), vec2(1.0));\n  vec4 field = texture(uField, uv);\n  vec4 aux = texture(uAux, uv);\n  vec3 visible = uLight > 0.5 ? field.rgb + (1.0 - field.a) : field.rgb;\n  float luma = dot(visible, vec3(0.2126, 0.7152, 0.0722));\n  float value = uLight > 0.5 ? clamp((1.0 - luma) * 1.5, 0.0, 1.0) : luma;\n  float pick = hash12(cell + uSeedValue * 7.31);\n  float level = hash12(cell * 1.73 + 4.1 + uSeedValue);\n  float tick = floor(uGlyphTime * 9.0 + level * 9.0);\n  float wobble = (hash12(cell * 2.31 + tick * 17.7) - 0.5) * uFlicker * 0.4 + (level - 0.5) * 0.1;\n  float tone = clamp(pow(value, 0.75) + wobble, 0.0, 1.0);\n  float index = floor(tone * (uCount - 1.0) + 0.5);\n  float decode = max(aux.g, 1.0 - aux.r);\n  if (decode > 0.02) {\n    float frame = floor(uGlyphTime * 20.0);\n    if (hash12(cell * 3.7 + frame * 5.3) < decode * 1.25) index = 1.0 + floor(hash12(cell + frame * 1.37) * (uCount - 1.0));\n  }\n  float present = step(pick, uDensity * mix(1.0, 0.3 + tone * 1.1, 0.55));\n  float cutoff = smoothstep(0.03, 0.1, value);\n  float focus = step(hash12(cell * 0.71 + 9.2 + uSeedValue), uDepth);\n  float coverage = sampleGlyph(index, local, coord, focus * 1.2);\n  float alpha = uOpacity * present * cutoff * coverage * mix(1.0, 0.35 + 0.65 * level, 0.55) * (1.0 - focus * 0.5);\n  if (uLight < 0.5) {\n    vec3 light = field.rgb * 0.55 + vec3(0.03) * field.a;\n    return vec4(light * alpha, 0.6 * field.a * alpha);\n  }\n  vec3 ink = visible * 0.8;\n  return vec4(ink * alpha, alpha);\n}\n`;\n\nconst HALO = `#version 300 es\nprecision highp float;\nuniform float uDownscale;\nin vec2 vUv;\nout vec4 outColor;\n${GLYPHS}\nvoid main() {\n  outColor = glyphAt(gl_FragCoord.xy * uDownscale);\n}`;\n\nconst BLUR = `#version 300 es\nprecision highp float;\nuniform sampler2D uSource;\nuniform vec2 uStep;\nin vec2 vUv;\nout vec4 outColor;\nvoid main() {\n  vec4 sum = texture(uSource, vUv) * 0.2270270270;\n  sum += texture(uSource, vUv + uStep * 1.3846153846) * 0.3162162162;\n  sum += texture(uSource, vUv - uStep * 1.3846153846) * 0.3162162162;\n  sum += texture(uSource, vUv + uStep * 3.2307692308) * 0.0702702703;\n  sum += texture(uSource, vUv - uStep * 3.2307692308) * 0.0702702703;\n  outColor = sum;\n}`;\n\nconst COMPOSITE = `#version 300 es\nprecision highp float;\nuniform sampler2D uHalo;\nuniform float uGlow;\nuniform float uGradient;\nuniform float uGrain;\nuniform float uFrame;\nin vec2 vUv;\nout vec4 outColor;\n${GLYPHS}\nvoid main() {\n  vec4 base = texture(uField, vUv) * uGradient;\n  vec4 glyph = glyphAt(gl_FragCoord.xy);\n  vec4 halo = texture(uHalo, vUv) * uGlow;\n  vec4 color;\n  if (uLight < 0.5) {\n    color = base + glyph + halo;\n  } else {\n    color = glyph + base * (1.0 - glyph.a);\n    color += halo * 0.6 * (1.0 - color.a);\n  }\n  float grain = (hash12(gl_FragCoord.xy + uFrame * vec2(17.0, 59.0)) + hash12(gl_FragCoord.xy * 1.31 + uFrame * 7.0) - 1.0) * uGrain;\n  float body = clamp(color.a, 0.0, 1.0);\n  color.rgb += grain * (0.25 + body) * 0.5;\n  color.a = clamp(color.a, 0.0, 1.0);\n  color.rgb = clamp(color.rgb, 0.0, color.a);\n  outColor = color;\n}`;\n\nconst clamp = (value, min, max) => Math.min(max, Math.max(min, value));\n\nconst toLinear = value => (value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4);\n\nconst toLab = ([r, g, b]) => {\n  const lr = toLinear(r);\n  const lg = toLinear(g);\n  const lb = toLinear(b);\n  const l = Math.cbrt(0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb);\n  const m = Math.cbrt(0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb);\n  const s = Math.cbrt(0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb);\n  return [\n    0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,\n    1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,\n    0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s\n  ];\n};\n\nconst compile = (gl, type, source) => {\n  const shader = gl.createShader(type);\n  if (!shader) return null;\n  gl.shaderSource(shader, source);\n  gl.compileShader(shader);\n  return shader;\n};\n\nconst link = (gl, fragmentSource) => {\n  const vertex = compile(gl, gl.VERTEX_SHADER, VERTEX);\n  const fragment = compile(gl, gl.FRAGMENT_SHADER, fragmentSource);\n  const program = gl.createProgram();\n  if (!vertex || !fragment || !program) return null;\n  gl.attachShader(program, vertex);\n  gl.attachShader(program, fragment);\n  gl.linkProgram(program);\n  gl.deleteShader(vertex);\n  gl.deleteShader(fragment);\n  return { program, uniforms: {} };\n};\n\nconst resolve = (gl, item) => {\n  if (!gl.getProgramParameter(item.program, gl.LINK_STATUS)) return false;\n  const count = gl.getProgramParameter(item.program, gl.ACTIVE_UNIFORMS);\n  for (let i = 0; i < count; i++) {\n    const info = gl.getActiveUniform(item.program, i);\n    if (!info) continue;\n    const name = info.name.replace(/\\[0\\]$/, '');\n    item.uniforms[name] = gl.getUniformLocation(item.program, name);\n  }\n  return true;\n};\n\nconst createTarget = (gl, count) => {\n  const textures = Array.from({ length: count }, () => {\n    const texture = gl.createTexture();\n    gl.bindTexture(gl.TEXTURE_2D, texture);\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    return texture;\n  });\n  return { textures, framebuffer: gl.createFramebuffer(), width: 0, height: 0 };\n};\n\nconst sizeTarget = (gl, target, width, height) => {\n  if (target.width === width && target.height === height) return;\n  target.width = width;\n  target.height = height;\n  gl.bindFramebuffer(gl.FRAMEBUFFER, target.framebuffer);\n  target.textures.forEach((texture, i) => {\n    gl.bindTexture(gl.TEXTURE_2D, texture);\n    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);\n    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + i, gl.TEXTURE_2D, texture, 0);\n  });\n  gl.bindFramebuffer(gl.FRAMEBUFFER, null);\n};\n\nconst buildAtlas = (gl, characters, fontFamily) => {\n  const list = [...new Set([...characters].filter(char => char !== '\\n' && char !== '\\r'))];\n  if (!list.length) list.push(' ');\n  const font = `300 ${Math.round(ATLAS_CELL_H * FONT_SCALE)}px ${fontFamily}`;\n  const columns = Math.min(ATLAS_COLUMNS, list.length);\n  const rows = Math.ceil(list.length / columns);\n  const canvas = document.createElement('canvas');\n  canvas.width = ATLAS_CELL_W * columns;\n  canvas.height = ATLAS_CELL_H * rows;\n  const context = canvas.getContext('2d');\n  if (context) {\n    context.font = font;\n    context.textAlign = 'center';\n    context.textBaseline = 'middle';\n    context.fillStyle = '#fff';\n    list.forEach((char, index) => {\n      const column = index % columns;\n      const row = Math.floor(index / columns);\n      context.fillText(char, column * ATLAS_CELL_W + ATLAS_CELL_W / 2, row * ATLAS_CELL_H + ATLAS_CELL_H / 2 + 3);\n    });\n  }\n  const texture = gl.createTexture();\n  gl.bindTexture(gl.TEXTURE_2D, texture);\n  gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);\n  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, gl.RGBA, gl.UNSIGNED_BYTE, canvas);\n  gl.generateMipmap(gl.TEXTURE_2D);\n  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_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  return { texture, count: list.length, grid: [columns, rows] };\n};\n\nconst GradientASCII = ({\n  colors = DEFAULT_COLORS,\n  shape = 'horizon',\n  characters = DEFAULT_CHARACTERS,\n  cellSize = 18,\n  density = 0.8,\n  glyphOpacity = 0.85,\n  glow = 0.2,\n  depthOfField = 0,\n  flicker = 0.2,\n  softness = 0.5,\n  scale = 1,\n  flow = 0.5,\n  speed = 1,\n  gradientOpacity = 0.9,\n  grain = 0.08,\n  variation = 0,\n  mouseInteraction = true,\n  scrambleRadius = 0.1,\n  clickEffect = 'wave',\n  intro = true,\n  lightMode = false,\n  fontFamily = DEFAULT_FONT,\n  dpr,\n  className = '',\n  style,\n  ...rest\n}) => {\n  const containerRef = useRef(null);\n  const canvasRef = useRef(null);\n  const wakeRef = useRef(null);\n  const palette = (Array.isArray(colors) && colors.length ? colors : DEFAULT_COLORS).slice(0, MAX_COLORS);\n  const settings = {\n    colors: palette.length > 1 ? palette : [palette[0], palette[0]],\n    shape: SHAPES[shape] ?? 0,\n    characters: characters || DEFAULT_CHARACTERS,\n    cellSize: clamp(cellSize, 6, 64),\n    density: clamp(density, 0, 1),\n    glyphOpacity: clamp(glyphOpacity, 0, 1),\n    glow: clamp(glow, 0, 2),\n    depthOfField: clamp(depthOfField, 0, 1),\n    flicker: clamp(flicker, 0, 1),\n    softness: clamp(softness, 0, 1),\n    scale: clamp(scale, 0.4, 2.5),\n    flow: clamp(flow, 0, 2),\n    speed: Math.max(0, speed),\n    gradientOpacity: clamp(gradientOpacity, 0, 1),\n    grain: clamp(grain, 0, 0.4),\n    variation: Math.max(0, variation),\n    mouseInteraction,\n    scrambleRadius: clamp(scrambleRadius, 0.02, 0.4),\n    clickEffect: CLICK_EFFECTS[clickEffect] ?? 1,\n    intro,\n    lightMode,\n    fontFamily: fontFamily || DEFAULT_FONT,\n    dpr\n  };\n  const settingsRef = useRef(settings);\n  settingsRef.current = settings;\n\n  useEffect(() => {\n    const container = containerRef.current;\n    const canvas = canvasRef.current;\n    const gl = canvas?.getContext('webgl2', {\n      alpha: true,\n      premultipliedAlpha: true,\n      antialias: false,\n      depth: false,\n      stencil: false\n    });\n    if (!container || !canvas || !gl) return undefined;\n\n    const parallel = gl.getExtension('KHR_parallel_shader_compile');\n    const field = link(gl, FIELD);\n    const halo = link(gl, HALO);\n    const blur = link(gl, BLUR);\n    const composite = link(gl, COMPOSITE);\n    if (!field || !halo || !blur || !composite) return undefined;\n    const programs = [field, halo, blur, composite];\n    if (!parallel && !programs.every(item => resolve(gl, item))) {\n      programs.forEach(item => gl.deleteProgram(item.program));\n      return undefined;\n    }\n    const vao = gl.createVertexArray();\n    const fieldTarget = createTarget(gl, 2);\n    const haloA = createTarget(gl, 1);\n    const haloB = createTarget(gl, 1);\n\n    const probe = document.createElement('canvas').getContext('2d', { willReadFrequently: true });\n    const colorCache = new Map();\n    const parse = value => {\n      const key = String(value);\n      const cached = colorCache.get(key);\n      if (cached) return cached;\n      let rgb = [0, 0, 0];\n      if (probe) {\n        probe.clearRect(0, 0, 1, 1);\n        probe.fillStyle = '#000000';\n        probe.fillStyle = key;\n        probe.fillRect(0, 0, 1, 1);\n        const [r, g, b] = probe.getImageData(0, 0, 1, 1).data;\n        rgb = [r / 255, g / 255, b / 255];\n      }\n      colorCache.set(key, rgb);\n      return rgb;\n    };\n\n    const reduce = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;\n    const view = { width: 1, height: 1, ratio: 1 };\n    const ramp = new Float32Array(MAX_COLORS * 3);\n    const clickData = new Float32Array(MAX_CLICKS * 4);\n    const clicks = [];\n    const trailData = new Float32Array(MAX_TRAIL * 4);\n    const trail = [];\n    const pointer = { x: 0, y: 0, inside: false };\n    let atlas = null;\n    let atlasKey = '';\n    let paletteKey = '';\n    let time = 0;\n    let glyphTime = 0;\n    let introClock = settingsRef.current.intro && !reduce ? 0 : 1;\n    let frameSeed = 0;\n    let grainClock = 0;\n    let raf = 0;\n    let last = 0;\n    let visible = true;\n    let disposed = false;\n    let ready = !parallel;\n\n    const linked = () =>\n      !parallel || programs.every(item => gl.getProgramParameter(item.program, parallel.COMPLETION_STATUS_KHR));\n\n    const release = () => {\n      if (gl.isContextLost()) return;\n      if (!ready && !linked()) {\n        requestAnimationFrame(release);\n        return;\n      }\n      programs.forEach(item => gl.deleteProgram(item.program));\n    };\n\n    const syncAtlas = () => {\n      const s = settingsRef.current;\n      const key = `${s.fontFamily}|${s.characters}`;\n      if (key === atlasKey && atlas) return;\n      atlasKey = key;\n      if (atlas) gl.deleteTexture(atlas.texture);\n      atlas = buildAtlas(gl, s.characters, s.fontFamily);\n      document.fonts?.load?.(`300 64px ${s.fontFamily}`, s.characters).then(\n        () => {\n          if (disposed || atlasKey !== key) return;\n          if (atlas) gl.deleteTexture(atlas.texture);\n          atlas = buildAtlas(gl, s.characters, s.fontFamily);\n          draw();\n        },\n        () => {}\n      );\n    };\n\n    const syncPalette = () => {\n      const s = settingsRef.current;\n      const key = s.colors.join(',');\n      if (key === paletteKey) return;\n      paletteKey = key;\n      s.colors.forEach((color, i) => ramp.set(toLab(parse(color)), i * 3));\n    };\n\n    const bindGlyphs = (program, width, height) => {\n      if (!atlas) return;\n      const s = settingsRef.current;\n      const u = program.uniforms;\n      const cellH = s.cellSize * view.ratio;\n      const cellW = cellH * (ATLAS_CELL_W / ATLAS_CELL_H);\n      gl.activeTexture(gl.TEXTURE0);\n      gl.bindTexture(gl.TEXTURE_2D, fieldTarget.textures[0]);\n      gl.uniform1i(u.uField, 0);\n      gl.activeTexture(gl.TEXTURE1);\n      gl.bindTexture(gl.TEXTURE_2D, fieldTarget.textures[1]);\n      gl.uniform1i(u.uAux, 1);\n      gl.activeTexture(gl.TEXTURE2);\n      gl.bindTexture(gl.TEXTURE_2D, atlas.texture);\n      gl.uniform1i(u.uAtlas, 2);\n      gl.uniform2f(u.uResolution, width, height);\n      gl.uniform2f(u.uCell, cellW, cellH);\n      gl.uniform2f(\n        u.uOffset,\n        (width - Math.ceil(width / cellW) * cellW) / 2,\n        (height - Math.ceil(height / cellH) * cellH) / 2\n      );\n      gl.uniform2f(u.uAtlasGrid, atlas.grid[0], atlas.grid[1]);\n      gl.uniform1f(u.uCount, atlas.count);\n      gl.uniform1f(u.uDensity, s.density);\n      gl.uniform1f(u.uOpacity, s.glyphOpacity);\n      gl.uniform1f(u.uFlicker, reduce ? 0 : s.flicker);\n      gl.uniform1f(u.uDepth, s.depthOfField);\n      gl.uniform1f(u.uGlyphTime, glyphTime);\n      gl.uniform1f(u.uSeedValue, s.variation);\n      gl.uniform1f(u.uLight, s.lightMode ? 1 : 0);\n    };\n\n    const draw = () => {\n      if (!ready) return;\n      const s = settingsRef.current;\n      const aspect = view.width / view.height;\n      syncAtlas();\n      syncPalette();\n      gl.bindVertexArray(vao);\n      gl.disable(gl.BLEND);\n\n      gl.bindFramebuffer(gl.FRAMEBUFFER, fieldTarget.framebuffer);\n      gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1]);\n      gl.viewport(0, 0, fieldTarget.width, fieldTarget.height);\n      gl.useProgram(field.program);\n      const fu = field.uniforms;\n      gl.uniform1f(fu.uAspect, aspect);\n      gl.uniform1f(fu.uTime, time);\n      gl.uniform1i(fu.uShape, s.shape);\n      gl.uniform3fv(fu.uRamp, ramp);\n      gl.uniform1i(fu.uCount, s.colors.length);\n      gl.uniform1f(fu.uScale, s.scale);\n      gl.uniform1f(fu.uSoftness, s.softness);\n      gl.uniform1f(fu.uFlow, s.flow);\n      gl.uniform2f(fu.uSeed, s.variation * 3.17, s.variation * 1.91);\n      gl.uniform4fv(fu.uTrail, trailData);\n      gl.uniform1f(fu.uRadius, s.scrambleRadius);\n      gl.uniform4fv(fu.uClicks, clickData);\n      gl.uniform1i(fu.uClickEffect, s.clickEffect);\n      gl.uniform1f(fu.uIntro, introClock);\n      gl.uniform1f(fu.uFrame, frameSeed);\n      gl.drawArrays(gl.TRIANGLES, 0, 3);\n\n      const glowing = s.glow > 0.001;\n      if (glowing) {\n        gl.bindFramebuffer(gl.FRAMEBUFFER, haloA.framebuffer);\n        gl.drawBuffers([gl.COLOR_ATTACHMENT0]);\n        gl.viewport(0, 0, haloA.width, haloA.height);\n        gl.useProgram(halo.program);\n        bindGlyphs(halo, canvas.width, canvas.height);\n        gl.uniform1f(halo.uniforms.uDownscale, canvas.width / haloA.width);\n        gl.drawArrays(gl.TRIANGLES, 0, 3);\n\n        gl.useProgram(blur.program);\n        gl.activeTexture(gl.TEXTURE0);\n        gl.uniform1i(blur.uniforms.uSource, 0);\n        gl.bindFramebuffer(gl.FRAMEBUFFER, haloB.framebuffer);\n        gl.bindTexture(gl.TEXTURE_2D, haloA.textures[0]);\n        gl.uniform2f(blur.uniforms.uStep, 1 / haloA.width, 0);\n        gl.drawArrays(gl.TRIANGLES, 0, 3);\n        gl.bindFramebuffer(gl.FRAMEBUFFER, haloA.framebuffer);\n        gl.bindTexture(gl.TEXTURE_2D, haloB.textures[0]);\n        gl.uniform2f(blur.uniforms.uStep, 0, 1 / haloA.height);\n        gl.drawArrays(gl.TRIANGLES, 0, 3);\n      }\n\n      gl.bindFramebuffer(gl.FRAMEBUFFER, null);\n      gl.viewport(0, 0, canvas.width, canvas.height);\n      gl.useProgram(composite.program);\n      bindGlyphs(composite, canvas.width, canvas.height);\n      const cu = composite.uniforms;\n      gl.activeTexture(gl.TEXTURE3);\n      gl.bindTexture(gl.TEXTURE_2D, haloA.textures[0]);\n      gl.uniform1i(cu.uHalo, 3);\n      gl.uniform1f(cu.uGlow, glowing ? s.glow : 0);\n      gl.uniform1f(cu.uGradient, s.gradientOpacity);\n      gl.uniform1f(cu.uGrain, s.grain);\n      gl.uniform1f(cu.uFrame, frameSeed);\n      gl.drawArrays(gl.TRIANGLES, 0, 3);\n    };\n\n    const resize = () => {\n      const s = settingsRef.current;\n      view.width = Math.max(1, container.clientWidth);\n      view.height = Math.max(1, container.clientHeight);\n      view.ratio = Math.min(s.dpr || window.devicePixelRatio || 1, 2);\n      canvas.width = Math.max(1, Math.round(view.width * view.ratio));\n      canvas.height = Math.max(1, Math.round(view.height * view.ratio));\n      sizeTarget(\n        gl,\n        fieldTarget,\n        Math.max(1, Math.round(canvas.width / 2)),\n        Math.max(1, Math.round(canvas.height / 2))\n      );\n      const haloWidth = Math.max(1, Math.round(canvas.width / 4));\n      const haloHeight = Math.max(1, Math.round(canvas.height / 4));\n      sizeTarget(gl, haloA, haloWidth, haloHeight);\n      sizeTarget(gl, haloB, haloWidth, haloHeight);\n      draw();\n      wake();\n    };\n\n    const updateInput = dt => {\n      trail.forEach(point => {\n        point.age += dt;\n      });\n      while (trail.length && trail[trail.length - 1].age > TRAIL_LIFE) trail.pop();\n      for (let i = 0; i < MAX_TRAIL; i++) {\n        const point = trail[i];\n        if (!point) {\n          trailData[i * 4 + 2] = 0;\n          continue;\n        }\n        const k = 1 - point.age / TRAIL_LIFE;\n        trailData.set([point.x, point.y, k * k, 0], i * 4);\n      }\n      clicks.forEach(click => {\n        click.age += dt;\n      });\n      while (clicks.length && clicks[clicks.length - 1].age > CLICK_LIFE) clicks.pop();\n      for (let i = 0; i < MAX_CLICKS; i++) {\n        const click = clicks[i];\n        if (!click) {\n          clickData[i * 4 + 3] = 0;\n          continue;\n        }\n        const k = click.age / CLICK_LIFE;\n        const envelope = Math.min(1, click.age / 0.08) * (1 - k) * (1 - k);\n        clickData.set([click.x, click.y, click.age, envelope], i * 4);\n      }\n    };\n\n    const frame = now => {\n      raf = 0;\n      if (disposed || !visible || document.hidden) return;\n      if (!ready) {\n        if (!gl.isContextLost() && !linked()) {\n          raf = requestAnimationFrame(frame);\n          return;\n        }\n        if (!programs.every(item => resolve(gl, item))) {\n          dispose();\n          return;\n        }\n        ready = true;\n      }\n      const s = settingsRef.current;\n      const dt = last ? clamp((now - last) / 1000, 0.001, 0.05) : 1 / 60;\n      last = now;\n      if (!reduce) {\n        time += dt * s.speed * 0.5;\n        glyphTime += dt * Math.max(s.speed, 0.15);\n      }\n      if (introClock < 1) introClock = Math.min(1, introClock + dt / INTRO_SECONDS);\n      grainClock += dt;\n      if (!reduce && grainClock > 1 / 24) {\n        grainClock = 0;\n        frameSeed = (frameSeed + 1) % 97;\n      }\n      updateInput(dt);\n      draw();\n      const busy = !reduce || introClock < 1 || clicks.length > 0 || trail.length > 0;\n      if (busy) raf = requestAnimationFrame(frame);\n      else last = 0;\n    };\n\n    function wake() {\n      if (!raf && !disposed && visible && !document.hidden) raf = requestAnimationFrame(frame);\n    }\n\n    const locate = event => {\n      const rect = container.getBoundingClientRect();\n      const x = (event.clientX - rect.left) / Math.max(1, rect.width);\n      const y = 1 - (event.clientY - rect.top) / Math.max(1, rect.height);\n      const aspect = rect.width / Math.max(1, rect.height);\n      pointer.inside = x >= 0 && x <= 1 && y >= 0 && y <= 1;\n      pointer.x = (x - 0.5) * aspect;\n      pointer.y = y - 0.5;\n    };\n\n    const onPointerMove = event => {\n      locate(event);\n      const s = settingsRef.current;\n      if (!pointer.inside || !s.mouseInteraction || reduce) return;\n      const head = trail[0];\n      const gap = head ? Math.hypot(pointer.x - head.x, pointer.y - head.y) : Infinity;\n      if (gap < TRAIL_STEP) return;\n      if (head && head.age < 0.1) {\n        const steps = Math.min(6, Math.floor(gap / (TRAIL_STEP * 2)));\n        for (let i = 1; i <= steps; i++) {\n          const k = i / (steps + 1);\n          trail.unshift({ x: head.x + (pointer.x - head.x) * k, y: head.y + (pointer.y - head.y) * k, age: 0 });\n        }\n      }\n      trail.unshift({ x: pointer.x, y: pointer.y, age: 0 });\n      trail.length = Math.min(trail.length, MAX_TRAIL);\n      wake();\n    };\n\n    const onPointerDown = event => {\n      locate(event);\n      const s = settingsRef.current;\n      if (!pointer.inside || !s.clickEffect || reduce) return;\n      clicks.unshift({ x: pointer.x, y: pointer.y, age: 0 });\n      if (clicks.length > MAX_CLICKS) clicks.pop();\n      wake();\n    };\n\n    const onVisibility = () => {\n      last = 0;\n      wake();\n    };\n\n    const resizeObserver = new ResizeObserver(resize);\n    resizeObserver.observe(container);\n    const intersection = new IntersectionObserver(entries => {\n      visible = entries.some(entry => entry.isIntersecting);\n      if (visible) {\n        last = 0;\n        wake();\n      }\n    });\n    intersection.observe(container);\n    window.addEventListener('pointermove', onPointerMove, { passive: true });\n    window.addEventListener('pointerdown', onPointerDown, { passive: true });\n    document.addEventListener('visibilitychange', onVisibility);\n    resize();\n    wakeRef.current = wake;\n\n    const dispose = () => {\n      if (disposed) return;\n      disposed = true;\n      cancelAnimationFrame(raf);\n      wakeRef.current = null;\n      resizeObserver.disconnect();\n      intersection.disconnect();\n      window.removeEventListener('pointermove', onPointerMove);\n      window.removeEventListener('pointerdown', onPointerDown);\n      document.removeEventListener('visibilitychange', onVisibility);\n      [fieldTarget, haloA, haloB].forEach(target => {\n        target.textures.forEach(texture => gl.deleteTexture(texture));\n        gl.deleteFramebuffer(target.framebuffer);\n      });\n      if (atlas) gl.deleteTexture(atlas.texture);\n      gl.deleteVertexArray(vao);\n      if (canvas.isConnected) {\n        release();\n      } else {\n        programs.forEach(item => gl.deleteProgram(item.program));\n        gl.getExtension('WEBGL_lose_context')?.loseContext();\n      }\n    };\n\n    return dispose;\n  }, []);\n\n  useEffect(() => {\n    wakeRef.current?.();\n  });\n\n  return (\n    <div\n      ref={containerRef}\n      className={['relative h-full w-full overflow-hidden', className].filter(Boolean).join(' ')}\n      style={style}\n      {...rest}\n    >\n      <canvas ref={canvasRef} className=\"absolute inset-0 block h-full w-full\" aria-hidden=\"true\" />\n    </div>\n  );\n};\n\nexport default GradientASCII;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": []
}