{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "backgrounds-glyph",
  "type": "registry:ui",
  "title": "Glyph Background",
  "description": "A background component featuring a field of characters lit by drifting glows, a cursor trail and waves on click.",
  "dependencies": [
    "@animate-ui/react",
    "cn",
    "motion"
  ],
  "files": [
    {
      "path": "registry/backgrounds/glyph/index.tsx",
      "type": "registry:ui",
      "target": "@components/animate-ui/backgrounds/glyph.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\nimport {\n  animate,\n  cancelFrame,\n  frame,\n  motionValue,\n  useInView,\n  type MotionValue,\n} from 'motion/react';\n\nimport {\n  ReducedMotionConfig,\n  useShouldReduceMotion,\n} from '@animate-ui/react/utils/motion';\nimport { usePageVisible } from '@animate-ui/react/utils/use-page-visible';\nimport { cn } from 'cn';\n\nconst CHARACTERS =\n  'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';\nconst MAX_WAVES = 12;\nconst TRAIL = 12;\nconst TRAIL_LIFE = 700;\nconst WAVE_LIFE = 4;\nconst WAVE_GAP = 0.3;\nconst WAVE_SPEED = 560;\nconst WAVE_SLOWDOWN = 0.66;\nconst WAVE_EDGE = 10;\nconst WAVE_TAIL = 30;\nconst DRIFT = [-30, 12] as const;\nconst ACTIVITY_DECAY = 900;\n\nconst VERTEX = `\nattribute vec2 position;\nvoid main() {\n  gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst FRAGMENT = `\nprecision highp float;\nuniform sampler2D atlas;\nuniform vec2 size;\nuniform vec2 cell;\nuniform float dpr;\nuniform float count;\nuniform float time;\nuniform float rest;\nuniform float haze;\nuniform float hoverInk;\nuniform float activity;\nuniform vec2 drift;\nuniform vec3 color;\nuniform vec3 accent;\nuniform vec4 waves[${MAX_WAVES * 2}];\nuniform vec4 trail[${TRAIL}];\n\nfloat hash(vec2 p) {\n  return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);\n}\n\nfloat smoothNoise(vec2 p) {\n  vec2 i = floor(p);\n  vec2 f = fract(p);\n  vec2 u = f * f * (3.0 - 2.0 * f);\n  return mix(\n    mix(hash(i), hash(i + vec2(1.0, 0.0)), u.x),\n    mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), u.x),\n    u.y\n  );\n}\n\nfloat clouds(vec2 p) {\n  float v = 0.0;\n  float a = 0.55;\n  for (int i = 0; i < 4; i++) {\n    v += a * smoothNoise(p);\n    p = p * 2.02 + vec2(17.1, 9.4);\n    a *= 0.5;\n  }\n  return v;\n}\n\nfloat outline(float kind, float angle, float phase) {\n  if (kind < 0.5) return 1.0;\n  if (kind < 1.5) {\n    float s = sin(angle);\n    float c = cos(angle);\n    return (2.0 - 2.0 * s + s * sqrt(abs(c)) / (s + 1.4)) * 0.42;\n  }\n  return 1.0 + 0.14 * sin(3.0 * angle + phase) + 0.08 * sin(5.0 * angle - phase * 1.3);\n}\n\nvoid main() {\n  vec2 pixel = vec2(gl_FragCoord.x, size.y - gl_FragCoord.y);\n  vec2 index = floor(pixel / cell);\n  vec2 point = (index + 0.5) * cell / dpr;\n\n  float ring = 0.0;\n  for (int i = 0; i < ${MAX_WAVES}; i++) {\n    vec4 a = waves[i * 2];\n    vec4 b = waves[i * 2 + 1];\n    if (a.w <= 0.0) continue;\n    vec2 v = point - a.xy;\n    float angle = atan(-v.y, v.x);\n    float shape = mix(1.0, outline(b.x, angle, b.z), b.y);\n    shape *= 1.0 + 0.07 * sin(3.0 * angle + b.z + b.w) + 0.05 * sin(5.0 * angle - b.z * 1.7 - b.w * 1.3) + 0.03 * sin(9.0 * angle + b.w * 2.1);\n    float d = length(v) - a.z * shape;\n    float k = d > 0.0 ? d / ${WAVE_EDGE.toFixed(1)} : d / ${WAVE_TAIL.toFixed(1)};\n    ring = max(ring, exp(-k * k) * a.w);\n  }\n\n  float hover = 0.0;\n  for (int i = 0; i < ${TRAIL}; i++) {\n    vec4 t = trail[i];\n    if (t.w <= 0.0) continue;\n    vec2 v = point - t.xy;\n    hover += t.w * exp(-dot(v, v) / (2.0 * t.z * t.z));\n  }\n  hover = min(hover, 1.0);\n\n  mat2 turn = mat2(0.82, 0.57, -0.57, 0.82);\n  vec2 q = turn * (point - drift);\n  float blob = smoothNoise(vec2(q.x / 140.0, q.y / 80.0) + vec2(time * 0.04, 0.0)) * 0.62\n    + smoothNoise(vec2(q.x / 62.0, q.y / 36.0) + vec2(3.7, time * 0.06)) * 0.38;\n  float grain = smoothNoise(vec2(q.x / 30.0, q.y / 14.0) + vec2(time * 0.2, 7.3));\n  float cloud = blob * (0.82 + 0.24 * grain);\n  float mask = pow(smoothstep(0.47 - 0.2 * activity, 0.84, cloud), 2.6);\n\n  float seed = hash(index);\n  float epoch = floor(time * 0.08 * (0.4 + seed) + seed * 97.0);\n  float character = floor(hash(index + epoch * 0.137) * count);\n  vec2 local = fract(pixel / cell);\n  float ink = pow(texture2D(atlas, vec2((character + local.x) / count, local.y)).a, 1.7);\n\n  float grey = ink * clamp(rest + haze * mask * (1.0 + 0.3 * activity) + hoverInk * hover * (0.6 + 0.4 * mask), 0.0, 1.0);\n  float blue = ink * ring * 0.72;\n  float alpha = grey + blue - grey * blue;\n  vec3 tint = color * grey * (1.0 - blue) + accent * blue;\n  gl_FragColor = vec4(tint, alpha);\n}\n`;\n\ntype Wave = {\n  x: number;\n  y: number;\n  kind: number;\n  phase: number;\n  strength: number;\n  age: MotionValue<number>;\n};\n\ntype GlyphBackgroundProps = React.ComponentProps<'div'> & {\n  glowColor?: string;\n  shapeColor?: string;\n  fontSize?: number;\n  interactive?: boolean;\n};\n\nfunction compile(gl: WebGLRenderingContext, type: number, source: string) {\n  const shader = gl.createShader(type);\n  if (!shader) return null;\n  gl.shaderSource(shader, source);\n  gl.compileShader(shader);\n  return gl.getShaderParameter(shader, gl.COMPILE_STATUS) ? shader : null;\n}\n\nfunction toRgb(color: string): [number, number, number] {\n  const probe = document.createElement('canvas').getContext('2d');\n  if (!probe) return [0.9, 0.9, 0.9];\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\nfunction drawAtlas(cellWidth: number, cellHeight: number, font: string) {\n  const atlas = document.createElement('canvas');\n  atlas.width = Math.ceil(cellWidth * CHARACTERS.length);\n  atlas.height = Math.ceil(cellHeight);\n  const context = atlas.getContext('2d')!;\n  context.font = font;\n  context.textAlign = 'center';\n  context.textBaseline = 'middle';\n  context.fillStyle = '#fff';\n  for (let i = 0; i < CHARACTERS.length; i++)\n    context.fillText(CHARACTERS[i]!, (i + 0.5) * cellWidth, cellHeight / 2);\n  return atlas;\n}\n\ntype Frame = {\n  width: number;\n  height: number;\n  dpr: number;\n  cell: [number, number];\n  time: number;\n  activity: number;\n  drift: [number, number];\n  color: [number, number, number];\n  accent: [number, number, number];\n  waves: Float32Array;\n  trail: Float32Array;\n};\n\nfunction createRenderer(gl: WebGLRenderingContext) {\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) return null;\n  gl.attachShader(program, vertex);\n  gl.attachShader(program, fragment);\n  gl.linkProgram(program);\n  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) return null;\n  gl.useProgram(program);\n\n  const buffer = gl.createBuffer();\n  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n  gl.bufferData(\n    gl.ARRAY_BUFFER,\n    new Float32Array([-1, -1, 3, -1, -1, 3]),\n    gl.STATIC_DRAW,\n  );\n  const position = gl.getAttribLocation(program, 'position');\n  gl.enableVertexAttribArray(position);\n  gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);\n\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\n  const at = (name: string) => gl.getUniformLocation(program, name);\n  const uniforms = {\n    atlas: at('atlas'),\n    size: at('size'),\n    cell: at('cell'),\n    dpr: at('dpr'),\n    count: at('count'),\n    time: at('time'),\n    rest: at('rest'),\n    haze: at('haze'),\n    hoverInk: at('hoverInk'),\n    activity: at('activity'),\n    drift: at('drift'),\n    color: at('color'),\n    accent: at('accent'),\n    waves: at('waves'),\n    trail: at('trail'),\n  };\n  gl.uniform1i(uniforms.atlas, 0);\n  gl.uniform1f(uniforms.count, CHARACTERS.length);\n  gl.uniform1f(uniforms.rest, 0);\n  gl.uniform1f(uniforms.haze, 0.1);\n  gl.uniform1f(uniforms.hoverInk, 0.3);\n\n  return {\n    atlas(image: HTMLCanvasElement) {\n      gl.bindTexture(gl.TEXTURE_2D, texture);\n      gl.texImage2D(\n        gl.TEXTURE_2D,\n        0,\n        gl.RGBA,\n        gl.RGBA,\n        gl.UNSIGNED_BYTE,\n        image,\n      );\n    },\n    draw(frame: Frame) {\n      gl.viewport(0, 0, frame.width, frame.height);\n      gl.uniform2f(uniforms.size, frame.width, frame.height);\n      gl.uniform2f(uniforms.cell, ...frame.cell);\n      gl.uniform1f(uniforms.dpr, frame.dpr);\n      gl.uniform1f(uniforms.time, frame.time);\n      gl.uniform1f(uniforms.activity, frame.activity);\n      gl.uniform2f(uniforms.drift, ...frame.drift);\n      gl.uniform3f(uniforms.color, ...frame.color);\n      gl.uniform3f(uniforms.accent, ...frame.accent);\n      gl.uniform4fv(uniforms.waves, frame.waves);\n      gl.uniform4fv(uniforms.trail, frame.trail);\n      gl.clearColor(0, 0, 0, 0);\n      gl.clear(gl.COLOR_BUFFER_BIT);\n      gl.drawArrays(gl.TRIANGLES, 0, 3);\n    },\n  };\n}\n\nfunction GlyphBackgroundContent({\n  ref,\n  glowColor,\n  shapeColor,\n  fontSize = 10,\n  interactive = true,\n  className,\n  children,\n  onPointerMove,\n  onPointerDown,\n  ...props\n}: GlyphBackgroundProps) {\n  const containerRef = React.useRef<HTMLDivElement>(null);\n  const canvasRef = React.useRef<HTMLCanvasElement>(null);\n  React.useImperativeHandle(ref, () => containerRef.current as HTMLDivElement);\n\n  const inView = useInView(containerRef);\n  const pageVisible = usePageVisible();\n  const reduce = useShouldReduceMotion();\n  const playing = inView && pageVisible && !reduce;\n\n  const wavesRef = React.useRef<Wave[]>([]);\n  const trailRef = React.useRef<{ x: number; y: number; t: number }[]>([]);\n  const drawRef = React.useRef<(time: number, looping?: boolean) => void>(\n    () => {},\n  );\n  const timeRef = React.useRef(0);\n  const resizeRef = React.useRef(() => {});\n  const activityRef = React.useRef({ value: 0, at: 0 });\n\n  const options = { glowColor, shapeColor, fontSize };\n  const optionsRef = React.useRef(options);\n  React.useLayoutEffect(() => {\n    optionsRef.current = options;\n  });\n\n  const spawn = (x: number, y: number) => {\n    const kind = 1 + Math.floor(Math.random() * 2);\n    const phase = Math.random() * Math.PI * 2;\n    for (const [delay, strength] of [\n      [0, 1],\n      [WAVE_GAP, 0.55],\n    ] as const) {\n      const wave: Wave = {\n        x,\n        y,\n        kind,\n        phase,\n        strength,\n        age: motionValue(0),\n      };\n      wavesRef.current = [...wavesRef.current.slice(1 - MAX_WAVES), wave];\n      animate(wave.age, WAVE_LIFE, {\n        duration: WAVE_LIFE,\n        delay,\n        ease: 'linear',\n      }).then(() => {\n        wavesRef.current = wavesRef.current.filter((w) => w !== wave);\n      });\n    }\n  };\n\n  React.useEffect(() => {\n    const canvas = canvasRef.current;\n    const container = containerRef.current;\n    if (!canvas || !container) return;\n    const gl = canvas.getContext('webgl', {\n      premultipliedAlpha: true,\n      preserveDrawingBuffer: true,\n      antialias: false,\n    });\n    const renderer = gl ? createRenderer(gl) : null;\n    if (!renderer) return;\n\n    let width = 0;\n    let height = 0;\n    let dpr = 1;\n    let cell: [number, number] = [7, 13];\n    let frames = 0;\n    let colorKey = '';\n    let color: [number, number, number] = [0.9, 0.9, 0.9];\n    let accent: [number, number, number] = [0.9, 0.9, 0.9];\n    const waves = new Float32Array(MAX_WAVES * 8);\n    const trail = new Float32Array(TRAIL * 4);\n\n    const resize = () => {\n      const rect = container.getBoundingClientRect();\n      width = rect.width;\n      height = rect.height;\n      dpr = Math.max(1, Math.min(window.devicePixelRatio || 1, 2));\n      canvas.width = Math.max(1, Math.round(width * dpr));\n      canvas.height = Math.max(1, Math.round(height * dpr));\n      canvas.style.width = `${width}px`;\n      canvas.style.height = `${height}px`;\n      const { fontSize } = optionsRef.current;\n      const font = `${fontSize * dpr}px Menlo, ui-monospace, SFMono-Regular, Consolas, monospace`;\n      const measure = document.createElement('canvas').getContext('2d')!;\n      measure.font = font;\n      cell = [\n        Math.round(measure.measureText('0').width),\n        Math.round(fontSize * 1.25 * dpr),\n      ];\n      renderer.atlas(drawAtlas(cell[0], cell[1], font));\n    };\n\n    const draw = (time: number, looping = false) => {\n      if (width === 0 || height === 0) return;\n      const { glowColor, shapeColor } = optionsRef.current;\n      if (!looping || frames++ % 30 === 0) {\n        const text = getComputedStyle(container).color || '#e5e5e5';\n        const key = `${glowColor ?? text}|${shapeColor ?? text}`;\n        if (key !== colorKey) {\n          colorKey = key;\n          color = toRgb(glowColor ?? text);\n          accent = toRgb(shapeColor ?? text);\n        }\n      }\n      waves.fill(0);\n      wavesRef.current.forEach((wave, i) => {\n        const age = wave.age.get();\n        if (age <= 0) return;\n        const radius = (WAVE_SPEED * age) / (1 + WAVE_SLOWDOWN * age);\n        const fade =\n          Math.min(1, age / 0.05) *\n          Math.exp(-Math.max(0, age - 0.4) / 1.3) *\n          Math.min(1, (WAVE_LIFE - age) / 0.5);\n        waves[i * 8] = wave.x;\n        waves[i * 8 + 1] = wave.y;\n        waves[i * 8 + 2] = radius;\n        waves[i * 8 + 3] = fade * wave.strength;\n        waves[i * 8 + 4] = wave.kind;\n        waves[i * 8 + 5] = 0.12 + 0.5 * Math.min(1, age / 2.2);\n        waves[i * 8 + 6] = wave.phase;\n        waves[i * 8 + 7] = age * 3;\n      });\n      trail.fill(0);\n      trailRef.current.forEach((point, i) => {\n        const age = (time - point.t) / TRAIL_LIFE;\n        if (age < 0 || age >= 1) return;\n        trail[i * 4] = point.x;\n        trail[i * 4 + 1] = point.y;\n        trail[i * 4 + 2] = 40 + 25 * age;\n        trail[i * 4 + 3] = 0.3 * (1 - age) * (1 - age);\n      });\n      const seconds = time / 1000;\n      const energy = activityRef.current;\n      const activity =\n        energy.value *\n        Math.exp(-Math.max(0, time - energy.at) / ACTIVITY_DECAY);\n      renderer.draw({\n        activity,\n        width: canvas.width,\n        height: canvas.height,\n        dpr,\n        cell,\n        time: seconds,\n        drift: [DRIFT[0] * seconds, DRIFT[1] * seconds],\n        color,\n        accent,\n        waves,\n        trail,\n      });\n    };\n\n    drawRef.current = draw;\n    resizeRef.current = resize;\n    resize();\n    draw(timeRef.current);\n    const observer = new ResizeObserver(() => {\n      resize();\n      draw(timeRef.current);\n    });\n    observer.observe(container);\n    return () => {\n      observer.disconnect();\n      drawRef.current = () => {};\n      resizeRef.current = () => {};\n    };\n  }, []);\n\n  React.useEffect(() => {\n    resizeRef.current();\n    drawRef.current(timeRef.current);\n  }, [fontSize]);\n\n  React.useEffect(() => {\n    drawRef.current(timeRef.current);\n  }, [glowColor, shapeColor]);\n\n  React.useEffect(() => {\n    if (!playing) return;\n    const tick = ({ delta }: { delta: number }) => {\n      timeRef.current += Math.min(delta, 64);\n      drawRef.current(timeRef.current, true);\n    };\n    frame.render(tick, true);\n    return () => cancelFrame(tick);\n  }, [playing]);\n\n  const point = (event: React.PointerEvent<HTMLDivElement>) => {\n    const rect = event.currentTarget.getBoundingClientRect();\n    return { x: event.clientX - rect.left, y: event.clientY - rect.top };\n  };\n\n  return (\n    <div\n      ref={containerRef}\n      data-slot=\"glyph-background\"\n      className={cn('relative size-full overflow-hidden', className)}\n      {...props}\n      onPointerMove={(event) => {\n        onPointerMove?.(event);\n        if (!interactive || reduce) return;\n        const { x, y } = point(event);\n        const now = timeRef.current;\n        const trail = trailRef.current;\n        const last = trail[trail.length - 1];\n        const energy = activityRef.current;\n        const current =\n          energy.value *\n          Math.exp(-Math.max(0, now - energy.at) / ACTIVITY_DECAY);\n        activityRef.current = {\n          value: Math.min(\n            1,\n            current + (last ? Math.hypot(x - last.x, y - last.y) : 0) / 600,\n          ),\n          at: now,\n        };\n        if (last && Math.hypot(x - last.x, y - last.y) < 18) return;\n        trailRef.current = [\n          ...trail.filter((p) => now - p.t < TRAIL_LIFE).slice(1 - TRAIL),\n          { x, y, t: now },\n        ];\n      }}\n      onPointerDown={(event) => {\n        onPointerDown?.(event);\n        if (!interactive || reduce) return;\n        const { x, y } = point(event);\n        spawn(x, y);\n      }}\n    >\n      <canvas\n        ref={canvasRef}\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute inset-0\"\n      />\n      {children}\n    </div>\n  );\n}\n\nfunction GlyphBackground(props: GlyphBackgroundProps) {\n  return (\n    <ReducedMotionConfig>\n      <GlyphBackgroundContent {...props} />\n    </ReducedMotionConfig>\n  );\n}\n\nexport { GlyphBackground, type GlyphBackgroundProps };\n"
    }
  ]
}