{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "banner-pattern",
  "title": "BannerPattern",
  "description": "The neon.com banner dot pattern as a live WebGL surface: a square-dot grid sampling a drifting green-to-amber field.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/banner-pattern/banner-pattern.tsx",
      "content": "\"use client\";\n\n/*\n * BannerPattern: neon.com's banner dot pattern as a live WebGL\n * surface — a square-dot grid masking a drifting color field, so\n * every dot samples the green-to-amber glow behind it. A brand\n * background for banners, cards, and covers. Static single frame\n * under reduced motion. GLSL lives in banner-pattern-shader.ts.\n */\n\nimport { useEffect, useRef } from \"react\";\nimport type { ComponentProps } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nimport { BANNER_FRAGMENT, BANNER_VERTEX } from \"./banner-pattern-shader\";\n\nexport type BannerPatternPalette = [\n  base: string,\n  green: string,\n  sage: string,\n  amber: string,\n  cream: string,\n  rust: string,\n];\n\nexport type BannerPatternProps = Omit<ComponentProps<\"canvas\">, \"children\"> & {\n  /** Animation speed multiplier; 0 freezes the field. */\n  speed?: number;\n  /** Grid cell size in CSS pixels. */\n  cell?: number;\n  /** 0-0.5 dot half-width as a fraction of the cell. */\n  dotSize?: number;\n  /** 0-1 unmasked ambient glow behind the dots. */\n  haze?: number;\n  /** 0-1 per-dot brightness variance. */\n  jitter?: number;\n  /** The field, painted back to front: [base, green, sage, amber, cream, rust]. */\n  colors?: BannerPatternPalette;\n};\n\n/**\n * Seconds for a dial or palette change to close ~63% of its gap —\n * uniforms ease toward their targets each frame, so a dragged slider\n * or a palette switch glides instead of snapping.\n */\nconst SMOOTH_TAU = 0.12;\n\nconst DEFAULT_COLORS: BannerPatternPalette = [\n  \"#0a0b09\",\n  \"#34d59a\",\n  \"#97b47d\",\n  \"#feaa2c\",\n  \"#ffeacc\",\n  \"#b03323\",\n];\n\nconst parseColor = (css: string): [number, number, number] => {\n  const probe = document.createElement(\"canvas\");\n  probe.width = 1;\n  probe.height = 1;\n  const context = probe.getContext(\"2d\");\n\n  if (!context) {\n    return [0, 0, 0];\n  }\n\n  context.fillStyle = css;\n  context.fillRect(0, 0, 1, 1);\n  const [r, g, b] = context.getImageData(0, 0, 1, 1).data;\n  return [(r ?? 0) / 255, (g ?? 0) / 255, (b ?? 0) / 255];\n};\n\nconst warnDev = (message: string) => {\n  if (typeof process !== \"undefined\" && process.env.NODE_ENV !== \"production\") {\n    console.warn(message);\n  }\n};\n\nconst compile = (\n  gl: WebGLRenderingContext,\n  type: number,\n  source: string\n): WebGLShader | null => {\n  const shader = gl.createShader(type);\n\n  if (!shader) {\n    return null;\n  }\n\n  gl.shaderSource(shader, source);\n  gl.compileShader(shader);\n\n  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n    warnDev(\n      `neon-ui: shader compile failed: ${gl.getShaderInfoLog(shader) ?? \"unknown\"}`\n    );\n    gl.deleteShader(shader);\n    return null;\n  }\n\n  return shader;\n};\n\nexport const BannerPattern = ({\n  cell = 8,\n  className,\n  colors = DEFAULT_COLORS,\n  dotSize = 0.14,\n  haze = 0.5,\n  jitter = 0.35,\n  speed = 0.5,\n  ...props\n}: BannerPatternProps) => {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  // Shader time, accumulated frame by frame so a speed change scales\n  // the flow from here instead of teleporting the whole field.\n  const phaseRef = useRef(0);\n  const lastFrameRef = useRef<number | null>(null);\n  // Smoothed uniform values, persisting across prop-driven re-inits.\n  const smoothedRef = useRef<Record<string, number> | null>(null);\n  const [base, green, sage, amber, cream, rust] = colors;\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    const gl = canvas?.getContext(\"webgl\", { alpha: false });\n\n    if (!(canvas && gl)) {\n      return;\n    }\n\n    const vertex = compile(gl, gl.VERTEX_SHADER, BANNER_VERTEX);\n    const fragment = compile(gl, gl.FRAGMENT_SHADER, BANNER_FRAGMENT);\n    const program = gl.createProgram();\n\n    if (!(vertex && fragment && program)) {\n      if (vertex) {\n        gl.deleteShader(vertex);\n      }\n      if (fragment) {\n        gl.deleteShader(fragment);\n      }\n      return;\n    }\n\n    gl.attachShader(program, vertex);\n    gl.attachShader(program, fragment);\n    gl.linkProgram(program);\n\n    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n      warnDev(\n        `neon-ui: program link failed: ${gl.getProgramInfoLog(program) ?? \"unknown\"}`\n      );\n      gl.deleteProgram(program);\n      gl.deleteShader(vertex);\n      gl.deleteShader(fragment);\n      return;\n    }\n\n    // oxlint-disable-next-line react/react-compiler -- WebGL method, not a React hook\n    gl.useProgram(program);\n\n    const quad = gl.createBuffer();\n    gl.bindBuffer(gl.ARRAY_BUFFER, quad);\n    gl.bufferData(\n      gl.ARRAY_BUFFER,\n      new Float32Array([-1, -1, 3, -1, -1, 3]),\n      gl.STATIC_DRAW\n    );\n    const position = gl.getAttribLocation(program, \"a_position\");\n    gl.enableVertexAttribArray(position);\n    gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);\n\n    const dpr = Math.min(window.devicePixelRatio || 1, 2);\n\n    const uResolution = gl.getUniformLocation(program, \"u_resolution\");\n    const uTime = gl.getUniformLocation(program, \"u_time\");\n    const uCell = gl.getUniformLocation(program, \"u_cell\");\n    const uDot = gl.getUniformLocation(program, \"u_dot\");\n    const uHaze = gl.getUniformLocation(program, \"u_haze\");\n    const uJitter = gl.getUniformLocation(program, \"u_jitter\");\n\n    const palette = [\n      [\"u_base\", base],\n      [\"u_green\", green],\n      [\"u_sage\", sage],\n      [\"u_amber\", amber],\n      [\"u_cream\", cream],\n      [\"u_rust\", rust],\n    ] as const;\n\n    const targets: Record<string, number> = {\n      cell,\n      dotSize,\n      haze,\n      jitter,\n      speed,\n    };\n\n    for (const [name, css] of palette) {\n      const [r, g, b] = parseColor(css);\n      targets[`${name}_r`] = r;\n      targets[`${name}_g`] = g;\n      targets[`${name}_b`] = b;\n    }\n\n    smoothedRef.current ??= { ...targets };\n    const smoothed = smoothedRef.current;\n    const colorLocations = palette.map(([name]) =>\n      gl.getUniformLocation(program, name)\n    );\n\n    /** Ease every uniform toward its target and upload; k=1 snaps. */\n    const applyUniforms = (k: number) => {\n      for (const key of Object.keys(targets)) {\n        const current = smoothed[key] ?? targets[key] ?? 0;\n        smoothed[key] = current + ((targets[key] ?? 0) - current) * k;\n      }\n\n      gl.uniform1f(uCell, (smoothed.cell ?? cell) * dpr);\n      gl.uniform1f(uDot, smoothed.dotSize ?? dotSize);\n      gl.uniform1f(uHaze, smoothed.haze ?? haze);\n      gl.uniform1f(uJitter, smoothed.jitter ?? jitter);\n\n      for (const [index, [name]] of palette.entries()) {\n        gl.uniform3f(\n          colorLocations[index] ?? null,\n          smoothed[`${name}_r`] ?? 0,\n          smoothed[`${name}_g`] ?? 0,\n          smoothed[`${name}_b`] ?? 0\n        );\n      }\n    };\n\n    let frame = 0;\n    let staticFrame = false;\n\n    const renderStatic = () => {\n      applyUniforms(1);\n      gl.uniform1f(uTime, 5);\n      gl.drawArrays(gl.TRIANGLES, 0, 3);\n    };\n\n    const resize = () => {\n      const width = Math.max(1, Math.round(canvas.clientWidth * dpr));\n      const height = Math.max(1, Math.round(canvas.clientHeight * dpr));\n\n      if (canvas.width !== width || canvas.height !== height) {\n        canvas.width = width;\n        canvas.height = height;\n        gl.viewport(0, 0, width, height);\n      }\n\n      gl.uniform2f(uResolution, canvas.width, canvas.height);\n    };\n\n    const observer = new ResizeObserver(() => {\n      resize();\n\n      if (staticFrame) {\n        renderStatic();\n      }\n    });\n    observer.observe(canvas);\n    resize();\n\n    const dispose = () => {\n      observer.disconnect();\n      gl.deleteBuffer(quad);\n      gl.deleteProgram(program);\n      gl.deleteShader(vertex);\n      gl.deleteShader(fragment);\n    };\n\n    const draw = (now: number) => {\n      const last = lastFrameRef.current ?? now;\n      lastFrameRef.current = now;\n      // Skip GL work while hidden (e.g. behind a docs Code overlay's\n      // visibility:hidden panel) — keep the loop alive, drop the cost.\n      if (!(canvas.checkVisibility?.() ?? true)) {\n        frame = requestAnimationFrame(draw);\n        return;\n      }\n\n      const dt = (now - last) / 1000;\n      applyUniforms(1 - Math.exp(-dt / SMOOTH_TAU));\n      phaseRef.current += dt * (smoothed.speed ?? speed);\n      gl.uniform1f(uTime, phaseRef.current);\n      gl.drawArrays(gl.TRIANGLES, 0, 3);\n      frame = requestAnimationFrame(draw);\n    };\n\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n\n    if (reduced.matches || speed === 0) {\n      staticFrame = true;\n      resize();\n      renderStatic();\n      return dispose;\n    }\n\n    frame = requestAnimationFrame(draw);\n\n    return () => {\n      cancelAnimationFrame(frame);\n      dispose();\n    };\n  }, [\n    amber,\n    base,\n    cell,\n    cream,\n    dotSize,\n    green,\n    haze,\n    jitter,\n    rust,\n    sage,\n    speed,\n  ]);\n\n  return (\n    <canvas\n      aria-hidden=\"true\"\n      className={cn(\"size-full\", className)}\n      data-slot=\"banner-pattern\"\n      ref={canvasRef}\n      {...props}\n    />\n  );\n};\n",
      "type": "registry:component"
    },
    {
      "path": "src/components/banner-pattern/banner-pattern-shader.ts",
      "content": "/*\n * The visual shader for BannerPattern, kept separate from the React\n * mount.\n *\n * A recreation of neon.com's banner-pattern.svg, which builds its dot\n * field in two layers:\n *\n * - a color field of big blurred gradient ellipses — Neon green\n *   upper-left, sage bridging the middle, amber strengthening right,\n *   a cream hot spot in the top-right corner, rust pooling low\n * - a square-dot grid used as an alpha MASK over that field, so each\n *   dot simply samples whatever color sits behind it\n *\n * On top of the mask the source adds fractal-noise grain and a faint\n * unmasked glow; here that becomes per-dot brightness jitter and a\n * dim ambient haze of the same field behind the dots.\n *\n * Fragment shader uniforms:\n * - u_resolution (vec2): canvas resolution in pixels\n * - u_time (float): animation time in seconds (pre-multiplied by speed)\n * - u_cell (float): grid cell size in device pixels\n * - u_dot (float): 0-0.5 dot half-width as a fraction of the cell\n * - u_haze (float): 0-1 unmasked ambient glow strength\n * - u_jitter (float): 0-1 per-dot brightness variance\n * - u_base / u_green / u_sage / u_amber / u_cream / u_rust (vec3):\n *   the palette, painted back to front\n */\n\nexport const BANNER_VERTEX = `\nattribute vec2 a_position;\nvoid main() {\n  gl_Position = vec4(a_position, 0.0, 1.0);\n}\n`;\n\nexport const BANNER_FRAGMENT = `\nprecision mediump float;\nuniform vec2 u_resolution;\nuniform float u_time;\nuniform float u_cell;\nuniform float u_dot;\nuniform float u_haze;\nuniform float u_jitter;\nuniform vec3 u_base;\nuniform vec3 u_green;\nuniform vec3 u_sage;\nuniform vec3 u_amber;\nuniform vec3 u_cream;\nuniform vec3 u_rust;\n\nfloat hash(vec2 p) {\n  return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);\n}\n\n/* Gaussian falloff, elongated by the per-axis stretch. */\nfloat blob(vec2 p, vec2 center, float radius, vec2 stretch) {\n  vec2 d = (p - center) / stretch;\n  return exp(-dot(d, d) / (radius * radius));\n}\n\n/* A slow elliptical orbit unique to each blob — fast enough that the\n   dots visibly change color at speed 1 and clearly flow at speed 3. */\nvec2 orbit(float t, float phase, float amount) {\n  return vec2(cos(t * 0.3 + phase), sin(t * 0.2 + phase * 1.7)) * amount;\n}\n\n/* The color field the dots sample — blobs painted back to front. */\nvec3 field(vec2 p, float aspect, float t) {\n  vec3 color = u_base;\n\n  float green = blob(\n    p, vec2(0.22 * aspect, 0.98) + orbit(t, 0.0, 0.09), 0.55, vec2(1.2, 1.0));\n  color = mix(color, u_green, min(green * 0.85, 1.0));\n\n  float sage = blob(\n    p, vec2(0.52 * aspect, 0.72) + orbit(t, 1.9, 0.1), 0.45, vec2(1.2, 1.0));\n  color = mix(color, u_sage, min(sage * 0.6, 1.0));\n\n  float amber = blob(\n    p, vec2(0.86 * aspect, 0.78) + orbit(t, 3.4, 0.09), 0.5, vec2(1.1, 1.1));\n  color = mix(color, u_amber, min(amber * 0.9, 1.0));\n\n  float cream = blob(\n    p, vec2(1.03 * aspect, 1.02) + orbit(t, 4.8, 0.06), 0.24, vec2(1.0, 1.0));\n  color = mix(color, u_cream, min(cream * 0.9, 1.0));\n\n  float rust = blob(\n    p, vec2(0.92 * aspect, 0.12) + orbit(t, 5.9, 0.09), 0.45, vec2(1.35, 1.0));\n  color = mix(color, u_rust, min(rust * 0.6, 1.0));\n\n  return color;\n}\n\n/* An antialiased square dot centered in each grid cell. */\nfloat dotMask(vec2 fragPx, float cell, float halfWidth) {\n  vec2 f = abs(fract(fragPx / cell) - 0.5);\n  float aa = 1.0 / cell;\n  return smoothstep(halfWidth + aa, halfWidth - aa, f.x) *\n    smoothstep(halfWidth + aa, halfWidth - aa, f.y);\n}\n\nvoid main() {\n  float aspect = u_resolution.x / u_resolution.y;\n  vec2 uv = gl_FragCoord.xy / u_resolution;\n  vec2 p = vec2(uv.x * aspect, uv.y);\n  float t = u_time;\n\n  vec3 sampled = field(p, aspect, t);\n\n  // Per-dot brightness jitter, the shader's stand-in for the source's\n  // fractal-noise grain: each cell keeps its own fixed variance.\n  vec2 cellId = floor(gl_FragCoord.xy / u_cell);\n  float jitter = mix(1.0 - u_jitter * 0.5, 1.0, hash(cellId));\n\n  float mask = dotMask(gl_FragCoord.xy, u_cell, u_dot);\n\n  // The dots sample the field; a dim haze of the same field sits\n  // behind them, like the source's unmasked glow ellipse.\n  vec3 color = sampled * (u_haze * 0.18) + sampled * mask * jitter;\n\n  gl_FragColor = vec4(color, 1.0);\n}\n`;\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}