{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"webgl-background","type":"registry:component","title":"WebGL Background","description":"Fragment-shader hero backgrounds (aurora, silk, plasma, light rays, pixel blast with click ripples, dither) behind a single typed variant prop. Reduced-motion renders one static frame; no WebGL falls back to a matching CSS gradient.","author":"UI Lab","dependencies":["clsx","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/webgl-background.tsx","type":"registry:component","target":"@components/motion/webgl-background.tsx","content":"\"use client\";\n// ui-lab-ten.vercel.app/components/motion/webgl-background\n\n// Ported from motion-anything (nexu-io, Apache-2.0); aurora/silk/light-rays/pixel-blast/dither\n// upstream: reactbits.dev, redistributed with permission.\n\nimport { useReducedMotion } from \"motion/react\";\nimport { type RefObject, useEffect, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\n/* ------------------------------- variants -------------------------------- */\n\ntype AuroraProps = {\n  variant: \"aurora\";\n  /** Three hex colors sampled left-to-right across the aurora band. */\n  colorStops?: readonly [string, string, string];\n  /** Wave height multiplier. */\n  amplitude?: number;\n  /** Softness of the aurora's lower edge. */\n  blend?: number;\n};\n\ntype SilkProps = {\n  variant: \"silk\";\n  color?: string;\n  speed?: number;\n  scale?: number;\n  rotation?: number;\n  noiseIntensity?: number;\n};\n\ntype PlasmaProps = {\n  variant: \"plasma\";\n  color?: string;\n  speed?: number;\n  scale?: number;\n  opacity?: number;\n};\n\ntype LightRaysProps = {\n  variant: \"light-rays\";\n  /** Hex color of the light rays. */\n  color?: string;\n  /** Animation speed multiplier for the ray shimmer. */\n  speed?: number;\n  /** Angular spread of the ray cone; lower is a narrower, sharper beam. */\n  spread?: number;\n};\n\ntype PixelBlastProps = {\n  variant: \"pixel-blast\";\n  /** Hex color of the dithered pixel field. */\n  color?: string;\n  /** Size of each pixel cell, in px. */\n  pixelSize?: number;\n  /** Strength of the click-ripple effect. */\n  rippleIntensity?: number;\n};\n\ntype DitherProps = {\n  variant: \"dither\";\n  /** Hex color of the dithered wave. */\n  color?: string;\n  /** Wave animation speed. */\n  speed?: number;\n  /** Size of each dithered pixel cell, in px. */\n  pixelSize?: number;\n};\n\ntype WebGLVariantProps = {\n  aurora: Omit<AuroraProps, \"variant\">;\n  silk: Omit<SilkProps, \"variant\">;\n  plasma: Omit<PlasmaProps, \"variant\">;\n  \"light-rays\": Omit<LightRaysProps, \"variant\">;\n  \"pixel-blast\": Omit<PixelBlastProps, \"variant\">;\n  dither: Omit<DitherProps, \"variant\">;\n};\n\nexport type WebGLBackgroundVariant = keyof WebGLVariantProps;\n\nexport type WebGLBackgroundProps = {\n  [K in WebGLBackgroundVariant]: { variant: K; className?: string } & WebGLVariantProps[K];\n}[WebGLBackgroundVariant];\n\nexport const WEBGL_BACKGROUND_VARIANTS: WebGLBackgroundVariant[] = [\n  \"aurora\",\n  \"silk\",\n  \"plasma\",\n  \"light-rays\",\n  \"pixel-blast\",\n  \"dither\",\n];\n\nconst DEFAULT_AURORA_STOPS: readonly [string, string, string] = [\n  \"#5227ff\",\n  \"#7cff67\",\n  \"#5227ff\",\n];\n\n/** Rendered instead of the canvas when a browser has no WebGL context. */\nconst FALLBACK_GRADIENT: Record<WebGLBackgroundVariant, string> = {\n  aurora: \"linear-gradient(135deg, #5227ff, #7cff67)\",\n  silk: \"linear-gradient(135deg, #726c86, #2d2a38)\",\n  plasma: \"radial-gradient(circle at 30% 30%, #8c7dff, #120e24)\",\n  \"light-rays\": \"radial-gradient(circle at 50% -10%, #fff4d2, #0d0b08 70%)\",\n  \"pixel-blast\": \"radial-gradient(circle at 50% 50%, #b497cf, #100b18)\",\n  dither: \"linear-gradient(135deg, #808080, #111114)\",\n};\n\n/* --------------------------------- GLSL ----------------------------------- */\n/* Verbatim fragment shaders from motion-anything's aurora/silk/plasma/light-rays/pixel-blast/\n * dither recipes. Vertex shaders below mirror _fx/shaderbg.js's generic full-screen triangle. */\n\nconst VERT_GL1 = `attribute vec2 position;\nattribute vec2 uv;\nvarying vec2 vUv;\nvoid main(){ vUv=uv; gl_Position=vec4(position,0.0,1.0); }\n`;\n\nconst VERT_GL2 = `#version 300 es\nin vec2 position;\nin vec2 uv;\nout vec2 vUv;\nvoid main(){ vUv=uv; gl_Position=vec4(position,0.0,1.0); }\n`;\n\nconst AURORA_FRAG = `#version 300 es\nprecision highp float;\nuniform float uTime; uniform float uAmplitude; uniform vec3 uColorStops[3]; uniform vec2 uResolution; uniform float uBlend;\nout vec4 fragColor;\nvec3 permute(vec3 x){ return mod(((x*34.0)+1.0)*x, 289.0); }\nfloat snoise(vec2 v){ const vec4 C = vec4(0.211324865405187,0.366025403784439,-0.577350269189626,0.024390243902439);\n  vec2 i=floor(v+dot(v,C.yy)); vec2 x0=v-i+dot(i,C.xx); vec2 i1=(x0.x>x0.y)?vec2(1.0,0.0):vec2(0.0,1.0);\n  vec4 x12=x0.xyxy+C.xxzz; x12.xy-=i1; i=mod(i,289.0);\n  vec3 p=permute(permute(i.y+vec3(0.0,i1.y,1.0))+i.x+vec3(0.0,i1.x,1.0));\n  vec3 m=max(0.5-vec3(dot(x0,x0),dot(x12.xy,x12.xy),dot(x12.zw,x12.zw)),0.0); m=m*m; m=m*m;\n  vec3 x=2.0*fract(p*C.www)-1.0; vec3 h=abs(x)-0.5; vec3 ox=floor(x+0.5); vec3 a0=x-ox;\n  m*=1.79284291400159-0.85373472095314*(a0*a0+h*h);\n  vec3 gg; gg.x=a0.x*x0.x+h.x*x0.y; gg.yz=a0.yz*x12.xz+h.yz*x12.yw; return 130.0*dot(m,gg); }\nstruct ColorStop { vec3 color; float position; };\n#define COLOR_RAMP(colors,factor,finalColor){ int index=0; for(int i=0;i<2;i++){ ColorStop cc=colors[i]; bool ib=cc.position<=factor; index=int(mix(float(index),float(i),float(ib))); } ColorStop cc=colors[index]; ColorStop nc=colors[index+1]; float range=nc.position-cc.position; float lf=(factor-cc.position)/range; finalColor=mix(cc.color,nc.color,lf); }\nvoid main(){ vec2 uv=gl_FragCoord.xy/uResolution;\n  ColorStop colors[3]; colors[0]=ColorStop(uColorStops[0],0.0); colors[1]=ColorStop(uColorStops[1],0.5); colors[2]=ColorStop(uColorStops[2],1.0);\n  vec3 rampColor; COLOR_RAMP(colors, uv.x, rampColor);\n  float height=snoise(vec2(uv.x*2.0+uTime*0.1, uTime*0.25))*0.5*uAmplitude; height=exp(height); height=(uv.y*2.0-height+0.2);\n  float intensity=0.6*height; float midPoint=0.20; float aa=smoothstep(midPoint-uBlend*0.5, midPoint+uBlend*0.5, intensity);\n  vec3 auroraColor=intensity*rampColor; fragColor=vec4(auroraColor*aa, aa); }\n`;\n\nconst SILK_FRAG = `precision highp float;\n\nvarying vec2 vUv;\n\nuniform float uTime;\nuniform vec3  uColor;\nuniform float uSpeed;\nuniform float uScale;\nuniform float uRotation;\nuniform float uNoiseIntensity;\n\nconst float e = 2.71828182845904523536;\n\nfloat noise(vec2 texCoord) {\n  float G = e;\n  vec2  r = (G * sin(G * texCoord));\n  return fract(r.x * r.y * (1.0 + texCoord.x));\n}\n\nvec2 rotateUvs(vec2 uv, float angle) {\n  float c = cos(angle);\n  float s = sin(angle);\n  mat2  rot = mat2(c, -s, s, c);\n  return rot * uv;\n}\n\nvoid main() {\n  float rnd        = noise(gl_FragCoord.xy);\n  vec2  uv         = rotateUvs(vUv * uScale, uRotation);\n  vec2  tex        = uv * uScale;\n  float tOffset    = uSpeed * uTime;\n\n  tex.y += 0.03 * sin(8.0 * tex.x - tOffset);\n\n  float pattern = 0.6 +\n                  0.4 * sin(5.0 * (tex.x + tex.y +\n                                   cos(3.0 * tex.x + 5.0 * tex.y) +\n                                   0.02 * tOffset) +\n                           sin(20.0 * (tex.x + tex.y - 0.1 * tOffset)));\n\n  vec4 col = vec4(uColor, 1.0) * vec4(pattern) - rnd / 15.0 * uNoiseIntensity;\n  col.a = 1.0;\n  gl_FragColor = col;\n}\n`;\n\nconst PLASMA_FRAG = `#version 300 es\nprecision highp float;\nuniform vec2 iResolution;\nuniform float iTime;\nuniform vec3 uCustomColor;\nuniform float uUseCustomColor;\nuniform float uSpeed;\nuniform float uDirection;\nuniform float uScale;\nuniform float uOpacity;\nuniform vec2 uMouse;\nuniform float uMouseInteractive;\nout vec4 fragColor;\n\nvoid mainImage(out vec4 o, vec2 C) {\n  vec2 center = iResolution.xy * 0.5;\n  C = (C - center) / uScale + center;\n\n  vec2 mouseOffset = (uMouse - center) * 0.0002;\n  C += mouseOffset * length(C - center) * step(0.5, uMouseInteractive);\n\n  float i, d, z, T = iTime * uSpeed * uDirection;\n  vec3 O, p, S;\n\n  for (vec2 r = iResolution.xy, Q; ++i < 60.; O += o.w/d*o.xyz) {\n    p = z*normalize(vec3(C-.5*r,r.y));\n    p.z -= 4.;\n    S = p;\n    d = p.y-T;\n\n    p.x += .4*(1.+p.y)*sin(d + p.x*0.1)*cos(.34*d + p.x*0.05);\n    Q = p.xz *= mat2(cos(p.y+vec4(0,11,33,0)-T));\n    z+= d = abs(sqrt(length(Q*Q)) - .25*(5.+S.y))/3.+8e-4;\n    o = 1.+sin(S.y+p.z*.5+S.z-length(S-p)+vec4(2,1,0,8));\n  }\n\n  o.xyz = tanh(O/1e4);\n}\n\nbool finite1(float x){ return !(isnan(x) || isinf(x)); }\nvec3 sanitize(vec3 c){\n  return vec3(\n    finite1(c.r) ? c.r : 0.0,\n    finite1(c.g) ? c.g : 0.0,\n    finite1(c.b) ? c.b : 0.0\n  );\n}\n\nvoid main() {\n  vec4 o = vec4(0.0);\n  mainImage(o, gl_FragCoord.xy);\n  vec3 rgb = sanitize(o.rgb);\n\n  float intensity = (rgb.r + rgb.g + rgb.b) / 3.0;\n  vec3 customColor = intensity * uCustomColor;\n  vec3 finalColor = mix(rgb, customColor, step(0.5, uUseCustomColor));\n\n  float alpha = length(rgb) * uOpacity;\n  fragColor = vec4(finalColor, alpha);\n}`;\n\nconst LIGHT_RAYS_FRAG = `precision highp float;\n\nuniform float iTime;\nuniform vec2  iResolution;\n\nuniform vec3  raysColor;\nuniform float raysSpeed;\nuniform float lightSpread;\nuniform float rayLength;\nuniform float pulsating;\nuniform float fadeDistance;\nuniform float saturation;\nuniform vec2  mousePos;\nuniform float mouseInfluence;\nuniform float noiseAmount;\nuniform float distortion;\n\nvarying vec2 vUv;\n\nfloat noise(vec2 st) {\n  return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123);\n}\n\nfloat rayStrength(vec2 raySource, vec2 rayRefDirection, vec2 coord,\n                  float seedA, float seedB, float speed) {\n  vec2 sourceToCoord = coord - raySource;\n  vec2 dirNorm = normalize(sourceToCoord);\n  float cosAngle = dot(dirNorm, rayRefDirection);\n\n  float distortedAngle = cosAngle + distortion * sin(iTime * 2.0 + length(sourceToCoord) * 0.01) * 0.2;\n\n  float spreadFactor = pow(max(distortedAngle, 0.0), 1.0 / max(lightSpread, 0.001));\n\n  float distance = length(sourceToCoord);\n  float maxDistance = iResolution.x * rayLength;\n  float lengthFalloff = clamp((maxDistance - distance) / maxDistance, 0.0, 1.0);\n\n  float fadeFalloff = clamp((iResolution.x * fadeDistance - distance) / (iResolution.x * fadeDistance), 0.5, 1.0);\n  float pulse = pulsating > 0.5 ? (0.8 + 0.2 * sin(iTime * speed * 3.0)) : 1.0;\n\n  float baseStrength = clamp(\n    (0.45 + 0.15 * sin(distortedAngle * seedA + iTime * speed)) +\n    (0.3 + 0.2 * cos(-distortedAngle * seedB + iTime * speed)),\n    0.0, 1.0\n  );\n\n  return baseStrength * lengthFalloff * fadeFalloff * spreadFactor * pulse;\n}\n\nvoid mainImage(out vec4 fragColor, in vec2 fragCoord) {\n  vec2 coord = vec2(fragCoord.x, iResolution.y - fragCoord.y);\n  vec2 rayPos = vec2(iResolution.x * 0.5, -0.2 * iResolution.y);\n  vec2 rayDir = vec2(0.0, 1.0);\n\n  vec2 finalRayDir = rayDir;\n  if (mouseInfluence > 0.0) {\n    vec2 mouseScreenPos = mousePos * iResolution.xy;\n    vec2 mouseDirection = normalize(mouseScreenPos - rayPos);\n    finalRayDir = normalize(mix(rayDir, mouseDirection, mouseInfluence));\n  }\n\n  vec4 rays1 = vec4(1.0) *\n               rayStrength(rayPos, finalRayDir, coord, 36.2214, 21.11349,\n                           1.5 * raysSpeed);\n  vec4 rays2 = vec4(1.0) *\n               rayStrength(rayPos, finalRayDir, coord, 22.3991, 18.0234,\n                           1.1 * raysSpeed);\n\n  fragColor = rays1 * 0.5 + rays2 * 0.4;\n\n  if (noiseAmount > 0.0) {\n    float n = noise(coord * 0.01 + iTime * 0.1);\n    fragColor.rgb *= (1.0 - noiseAmount + noiseAmount * n);\n  }\n\n  float brightness = 1.0 - (coord.y / iResolution.y);\n  fragColor.x *= 0.1 + brightness * 0.8;\n  fragColor.y *= 0.3 + brightness * 0.6;\n  fragColor.z *= 0.5 + brightness * 0.5;\n\n  if (saturation != 1.0) {\n    float gray = dot(fragColor.rgb, vec3(0.299, 0.587, 0.114));\n    fragColor.rgb = mix(vec3(gray), fragColor.rgb, saturation);\n  }\n\n  fragColor.rgb *= raysColor;\n}\n\nvoid main() {\n  vec4 color;\n  mainImage(color, gl_FragCoord.xy);\n  gl_FragColor  = color;\n}\n`;\n\nconst PIXEL_BLAST_FRAG = `#version 300 es\nprecision highp float;\nprecision highp float;\n\nuniform vec3  uColor;\nuniform vec2  uResolution;\nuniform float uTime;\nuniform float uPixelSize;\nuniform float uScale;\nuniform float uDensity;\nuniform float uPixelJitter;\nuniform int   uEnableRipples;\nuniform float uRippleSpeed;\nuniform float uRippleThickness;\nuniform float uRippleIntensity;\nuniform float uEdgeFade;\n\nuniform int   uShapeType;\nconst int SHAPE_SQUARE   = 0;\nconst int SHAPE_CIRCLE   = 1;\nconst int SHAPE_TRIANGLE = 2;\nconst int SHAPE_DIAMOND  = 3;\n\nconst int   MAX_CLICKS = 10;\n\nuniform vec2  uClickPos  [MAX_CLICKS];\nuniform float uClickTimes[MAX_CLICKS];\n\nout vec4 fragColor;\n\nfloat Bayer2(vec2 a) {\n  a = floor(a);\n  return fract(a.x / 2. + a.y * a.y * .75);\n}\n#define Bayer4(a) (Bayer2(.5*(a))*0.25 + Bayer2(a))\n#define Bayer8(a) (Bayer4(.5*(a))*0.25 + Bayer2(a))\n\n#define FBM_OCTAVES     5\n#define FBM_LACUNARITY  1.25\n#define FBM_GAIN        1.0\n\nfloat hash11(float n){ return fract(sin(n)*43758.5453); }\n\nfloat vnoise(vec3 p){\n  vec3 ip = floor(p);\n  vec3 fp = fract(p);\n  float n000 = hash11(dot(ip + vec3(0.0,0.0,0.0), vec3(1.0,57.0,113.0)));\n  float n100 = hash11(dot(ip + vec3(1.0,0.0,0.0), vec3(1.0,57.0,113.0)));\n  float n010 = hash11(dot(ip + vec3(0.0,1.0,0.0), vec3(1.0,57.0,113.0)));\n  float n110 = hash11(dot(ip + vec3(1.0,1.0,0.0), vec3(1.0,57.0,113.0)));\n  float n001 = hash11(dot(ip + vec3(0.0,0.0,1.0), vec3(1.0,57.0,113.0)));\n  float n101 = hash11(dot(ip + vec3(1.0,0.0,1.0), vec3(1.0,57.0,113.0)));\n  float n011 = hash11(dot(ip + vec3(0.0,1.0,1.0), vec3(1.0,57.0,113.0)));\n  float n111 = hash11(dot(ip + vec3(1.0,1.0,1.0), vec3(1.0,57.0,113.0)));\n  vec3 w = fp*fp*fp*(fp*(fp*6.0-15.0)+10.0);\n  float x00 = mix(n000, n100, w.x);\n  float x10 = mix(n010, n110, w.x);\n  float x01 = mix(n001, n101, w.x);\n  float x11 = mix(n011, n111, w.x);\n  float y0  = mix(x00, x10, w.y);\n  float y1  = mix(x01, x11, w.y);\n  return mix(y0, y1, w.z) * 2.0 - 1.0;\n}\n\nfloat fbm2(vec2 uv, float t){\n  vec3 p = vec3(uv * uScale, t);\n  float amp = 1.0;\n  float freq = 1.0;\n  float sum = 1.0;\n  for (int i = 0; i < FBM_OCTAVES; ++i){\n    sum  += amp * vnoise(p * freq);\n    freq *= FBM_LACUNARITY;\n    amp  *= FBM_GAIN;\n  }\n  return sum * 0.5 + 0.5;\n}\n\nfloat maskCircle(vec2 p, float cov){\n  float r = sqrt(cov) * .25;\n  float d = length(p - 0.5) - r;\n  float aa = 0.5 * fwidth(d);\n  return cov * (1.0 - smoothstep(-aa, aa, d * 2.0));\n}\n\nfloat maskTriangle(vec2 p, vec2 id, float cov){\n  bool flip = mod(id.x + id.y, 2.0) > 0.5;\n  if (flip) p.x = 1.0 - p.x;\n  float r = sqrt(cov);\n  float d  = p.y - r*(1.0 - p.x);\n  float aa = fwidth(d);\n  return cov * clamp(0.5 - d/aa, 0.0, 1.0);\n}\n\nfloat maskDiamond(vec2 p, float cov){\n  float r = sqrt(cov) * 0.564;\n  return step(abs(p.x - 0.49) + abs(p.y - 0.49), r);\n}\n\nvoid main(){\n  float pixelSize = uPixelSize;\n  vec2 fragCoord = gl_FragCoord.xy - uResolution * .5;\n  float aspectRatio = uResolution.x / uResolution.y;\n\n  vec2 pixelId = floor(fragCoord / pixelSize);\n  vec2 pixelUV = fract(fragCoord / pixelSize);\n\n  float cellPixelSize = 8.0 * pixelSize;\n  vec2 cellId = floor(fragCoord / cellPixelSize);\n  vec2 cellCoord = cellId * cellPixelSize;\n  vec2 uv = cellCoord / uResolution * vec2(aspectRatio, 1.0);\n\n  float base = fbm2(uv, uTime * 0.05);\n  base = base * 0.5 - 0.65;\n\n  float feed = base + (uDensity - 0.5) * 0.3;\n\n  float speed     = uRippleSpeed;\n  float thickness = uRippleThickness;\n  const float dampT     = 1.0;\n  const float dampR     = 10.0;\n\n  if (uEnableRipples == 1) {\n    for (int i = 0; i < MAX_CLICKS; ++i){\n      vec2 pos = uClickPos[i];\n      if (pos.x < 0.0) continue;\n      float cellPixelSize = 8.0 * pixelSize;\n      vec2 cuv = (((pos - uResolution * .5 - cellPixelSize * .5) / (uResolution))) * vec2(aspectRatio, 1.0);\n      float t = max(uTime - uClickTimes[i], 0.0);\n      float r = distance(uv, cuv);\n      float waveR = speed * t;\n      float ring  = exp(-pow((r - waveR) / thickness, 2.0));\n      float atten = exp(-dampT * t) * exp(-dampR * r);\n      feed = max(feed, ring * atten * uRippleIntensity);\n    }\n  }\n\n  float bayer = Bayer8(fragCoord / uPixelSize) - 0.5;\n  float bw = step(0.5, feed + bayer);\n\n  float h = fract(sin(dot(floor(fragCoord / uPixelSize), vec2(127.1, 311.7))) * 43758.5453);\n  float jitterScale = 1.0 + (h - 0.5) * uPixelJitter;\n  float coverage = bw * jitterScale;\n  float M;\n  if      (uShapeType == SHAPE_CIRCLE)   M = maskCircle (pixelUV, coverage);\n  else if (uShapeType == SHAPE_TRIANGLE) M = maskTriangle(pixelUV, pixelId, coverage);\n  else if (uShapeType == SHAPE_DIAMOND)  M = maskDiamond(pixelUV, coverage);\n  else                                   M = coverage;\n\n  if (uEdgeFade > 0.0) {\n    vec2 norm = gl_FragCoord.xy / uResolution;\n    float edge = min(min(norm.x, norm.y), min(1.0 - norm.x, 1.0 - norm.y));\n    float fade = smoothstep(0.0, uEdgeFade, edge);\n    M *= fade;\n  }\n\n  vec3 color = uColor;\n\n  // sRGB gamma correction - convert linear to sRGB for accurate color output\n  vec3 srgbColor = mix(\n    color * 12.92,\n    1.055 * pow(color, vec3(1.0 / 2.4)) - 0.055,\n    step(0.0031308, color)\n  );\n\n  fragColor = vec4(srgbColor * M, M);\n}\n`;\n\n/* Hand-merged from the react-bits two-pass original (wave shader -> retro Bayer-dither\n * postprocess): the wave is procedural, so sampling pass-1 at the pixelated uv is equivalent to\n * computing the wave AT that uv, so one #version 300 es fragment shader replicates both passes\n * with zero framebuffers. Verbatim from motion-anything's dither.js. */\nconst DITHER_FRAG = `#version 300 es\nprecision highp float;\nout vec4 fragColor;\nuniform vec2 uResolution;\nuniform float uTime;\nuniform vec2 uMouse;\nuniform float waveSpeed;\nuniform float waveFrequency;\nuniform float waveAmplitude;\nuniform vec3 waveColor;\nuniform int enableMouseInteraction;\nuniform float mouseRadius;\nuniform float colorNum;\nuniform float pixelSize;\nvec4 mod289(vec4 x){ return x - floor(x * (1.0/289.0)) * 289.0; }\nvec4 permute(vec4 x){ return mod289(((x * 34.0) + 1.0) * x); }\nvec4 taylorInvSqrt(vec4 r){ return 1.79284291400159 - 0.85373472095314 * r; }\nvec2 fade(vec2 t){ return t*t*t*(t*(t*6.0-15.0)+10.0); }\nfloat cnoise(vec2 P){\n  vec4 Pi = floor(P.xyxy) + vec4(0.0,0.0,1.0,1.0);\n  vec4 Pf = fract(P.xyxy) - vec4(0.0,0.0,1.0,1.0);\n  Pi = mod289(Pi);\n  vec4 ix = Pi.xzxz; vec4 iy = Pi.yyww; vec4 fx = Pf.xzxz; vec4 fy = Pf.yyww;\n  vec4 i = permute(permute(ix) + iy);\n  vec4 gx = fract(i * (1.0/41.0)) * 2.0 - 1.0;\n  vec4 gy = abs(gx) - 0.5; vec4 tx = floor(gx + 0.5); gx = gx - tx;\n  vec2 g00 = vec2(gx.x, gy.x); vec2 g10 = vec2(gx.y, gy.y);\n  vec2 g01 = vec2(gx.z, gy.z); vec2 g11 = vec2(gx.w, gy.w);\n  vec4 norm = taylorInvSqrt(vec4(dot(g00,g00), dot(g01,g01), dot(g10,g10), dot(g11,g11)));\n  g00 *= norm.x; g01 *= norm.y; g10 *= norm.z; g11 *= norm.w;\n  float n00 = dot(g00, vec2(fx.x, fy.x)); float n10 = dot(g10, vec2(fx.y, fy.y));\n  float n01 = dot(g01, vec2(fx.z, fy.z)); float n11 = dot(g11, vec2(fx.w, fy.w));\n  vec2 fade_xy = fade(Pf.xy);\n  vec2 n_x = mix(vec2(n00, n01), vec2(n10, n11), fade_xy.x);\n  return 2.3 * mix(n_x.x, n_x.y, fade_xy.y);\n}\nconst int OCTAVES = 4;\nfloat fbm(vec2 p){\n  float value = 0.0; float amp = 1.0; float freq = waveFrequency;\n  for (int i = 0; i < OCTAVES; i++){ value += amp * abs(cnoise(p)); p *= freq; amp *= waveAmplitude; }\n  return value;\n}\nfloat pattern(vec2 p){ vec2 p2 = p - uTime * waveSpeed; return fbm(p + fbm(p2)); }\nconst float bayerMatrix8x8[64] = float[64](\n  0.0/64.0, 48.0/64.0, 12.0/64.0, 60.0/64.0,  3.0/64.0, 51.0/64.0, 15.0/64.0, 63.0/64.0,\n  32.0/64.0,16.0/64.0, 44.0/64.0, 28.0/64.0, 35.0/64.0,19.0/64.0, 47.0/64.0, 31.0/64.0,\n  8.0/64.0, 56.0/64.0,  4.0/64.0, 52.0/64.0, 11.0/64.0,59.0/64.0,  7.0/64.0, 55.0/64.0,\n  40.0/64.0,24.0/64.0, 36.0/64.0, 20.0/64.0, 43.0/64.0,27.0/64.0, 39.0/64.0, 23.0/64.0,\n  2.0/64.0, 50.0/64.0, 14.0/64.0, 62.0/64.0,  1.0/64.0,49.0/64.0, 13.0/64.0, 61.0/64.0,\n  34.0/64.0,18.0/64.0, 46.0/64.0, 30.0/64.0, 33.0/64.0,17.0/64.0, 45.0/64.0, 29.0/64.0,\n  10.0/64.0,58.0/64.0,  6.0/64.0, 54.0/64.0,  9.0/64.0,57.0/64.0,  5.0/64.0, 53.0/64.0,\n  42.0/64.0,26.0/64.0, 38.0/64.0, 22.0/64.0, 41.0/64.0,25.0/64.0, 37.0/64.0, 21.0/64.0\n);\nvec3 dither(vec2 uv, vec3 color){\n  vec2 scaledCoord = floor(uv * uResolution / pixelSize);\n  int x = int(mod(scaledCoord.x, 8.0)); int y = int(mod(scaledCoord.y, 8.0));\n  float threshold = bayerMatrix8x8[y * 8 + x] - 0.25;\n  float step = 1.0 / (colorNum - 1.0);\n  color += threshold * step;\n  float bias = 0.2;\n  color = clamp(color - bias, 0.0, 1.0);\n  return floor(color * (colorNum - 1.0) + 0.5) / (colorNum - 1.0);\n}\nvoid main(){\n  vec2 uvScreen = gl_FragCoord.xy / uResolution;\n  vec2 normalizedPixelSize = pixelSize / uResolution;\n  vec2 uvPixel = normalizedPixelSize * floor(uvScreen / normalizedPixelSize);\n  vec2 fragPix = uvPixel * uResolution;\n  vec2 uv = fragPix / uResolution - 0.5;\n  uv.x *= uResolution.x / uResolution.y;\n  float f = pattern(uv);\n  if (enableMouseInteraction == 1) {\n    vec2 mouseNDC = uMouse - 0.5;\n    mouseNDC.x *= uResolution.x / uResolution.y;\n    float dist = length(uv - mouseNDC);\n    float effect = 1.0 - smoothstep(0.0, mouseRadius, dist);\n    f -= 0.5 * effect;\n  }\n  vec3 col = mix(vec3(0.0), waveColor, f);\n  col = dither(uvScreen, col);\n  fragColor = vec4(col, 1.0);\n}\n`;\n\n/* ------------------------------ gl plumbing ------------------------------- */\n\ntype GLContext = WebGLRenderingContext | WebGL2RenderingContext;\n\ntype UniformSpec =\n  | { type: \"1f\"; value: number }\n  | { type: \"1i\"; value: number }\n  | { type: \"2f\"; value: readonly [number, number] }\n  | { type: \"3f\"; value: readonly [number, number, number] }\n  | { type: \"1fv\"; value: readonly number[] }\n  | { type: \"2fv\"; value: readonly number[] }\n  | { type: \"3fv\"; value: readonly number[] };\n\n/** Click-ripple state for pixel-blast: a fixed-size circular buffer of canvas-space click\n * positions and shader-time timestamps, mirroring motion-anything's MAX_CLICKS=10 uClickPos/\n * uClickTimes uniforms. Lives in a component-level ref so it survives re-renders. */\ntype RippleState = {\n  positions: number[];\n  times: number[];\n  index: number;\n};\n\nconst MAX_RIPPLE_CLICKS = 10;\n\nfunction createRippleState(): RippleState {\n  return {\n    positions: new Array(MAX_RIPPLE_CLICKS * 2).fill(-1),\n    times: new Array(MAX_RIPPLE_CLICKS).fill(0),\n    index: 0,\n  };\n}\n\ntype ShaderConfig = {\n  frag: string;\n  uniforms: Record<string, UniformSpec>;\n  useMouse: boolean;\n  /** Multiplies the shader clock; only pixel-blast's upstream recipe sets this (0.5). */\n  timeScale?: number;\n  /** Click-ripple hook (pixel-blast only): fires on pointerdown with direct GL access so the\n   * variant can push its own uClickPos/uClickTimes uniforms without growing the generic runner. */\n  onPointerDown?: (ctx: {\n    gl: GLContext;\n    program: WebGLProgram;\n    canvasX: number;\n    canvasY: number;\n    time: number;\n  }) => void;\n};\n\nfunction hexToRgb(hex: string): [number, number, number] {\n  const clean = hex.replace(\"#\", \"\");\n  const value = Number.parseInt(clean, 16);\n  return [((value >> 16) & 255) / 255, ((value >> 8) & 255) / 255, (value & 255) / 255];\n}\n\nfunction buildShaderConfig(props: WebGLBackgroundProps, ripple: RippleState): ShaderConfig {\n  switch (props.variant) {\n    case \"aurora\": {\n      const stops = props.colorStops ?? DEFAULT_AURORA_STOPS;\n      const flat = stops.flatMap(hexToRgb);\n      return {\n        frag: AURORA_FRAG,\n        useMouse: false,\n        uniforms: {\n          uAmplitude: { type: \"1f\", value: props.amplitude ?? 1 },\n          uBlend: { type: \"1f\", value: props.blend ?? 0.5 },\n          uColorStops: { type: \"3fv\", value: flat },\n        },\n      };\n    }\n    case \"silk\": {\n      const rgb = hexToRgb(props.color ?? \"#7b7482\");\n      return {\n        frag: SILK_FRAG,\n        useMouse: false,\n        uniforms: {\n          uColor: { type: \"3f\", value: rgb },\n          uSpeed: { type: \"1f\", value: props.speed ?? 0.5 },\n          uScale: { type: \"1f\", value: props.scale ?? 1 },\n          uRotation: { type: \"1f\", value: props.rotation ?? 0 },\n          uNoiseIntensity: { type: \"1f\", value: props.noiseIntensity ?? 1.5 },\n        },\n      };\n    }\n    case \"plasma\": {\n      const rgb = hexToRgb(props.color ?? \"#8c7dff\");\n      return {\n        frag: PLASMA_FRAG,\n        useMouse: true,\n        uniforms: {\n          uCustomColor: { type: \"3f\", value: rgb },\n          uUseCustomColor: { type: \"1f\", value: 1 },\n          uSpeed: { type: \"1f\", value: props.speed ?? 1 },\n          uDirection: { type: \"1f\", value: 1 },\n          uScale: { type: \"1f\", value: props.scale ?? 1 },\n          uOpacity: { type: \"1f\", value: props.opacity ?? 1 },\n          uMouseInteractive: { type: \"1f\", value: 1 },\n        },\n      };\n    }\n    case \"light-rays\": {\n      const rgb = hexToRgb(props.color ?? \"#ffffff\");\n      return {\n        frag: LIGHT_RAYS_FRAG,\n        useMouse: false,\n        uniforms: {\n          raysColor: { type: \"3f\", value: rgb },\n          raysSpeed: { type: \"1f\", value: props.speed ?? 1 },\n          lightSpread: { type: \"1f\", value: props.spread ?? 1 },\n          rayLength: { type: \"1f\", value: 2 },\n          pulsating: { type: \"1f\", value: 0 },\n          fadeDistance: { type: \"1f\", value: 1 },\n          saturation: { type: \"1f\", value: 1 },\n          mousePos: { type: \"2f\", value: [0.5, 0.5] },\n          mouseInfluence: { type: \"1f\", value: 0 },\n          noiseAmount: { type: \"1f\", value: 0 },\n          distortion: { type: \"1f\", value: 0 },\n        },\n      };\n    }\n    case \"pixel-blast\": {\n      const rgb = hexToRgb(props.color ?? \"#b497cf\");\n      return {\n        frag: PIXEL_BLAST_FRAG,\n        useMouse: false,\n        timeScale: 0.5,\n        uniforms: {\n          uColor: { type: \"3f\", value: rgb },\n          uShapeType: { type: \"1i\", value: 0 },\n          uPixelSize: { type: \"1f\", value: props.pixelSize ?? 3 },\n          uScale: { type: \"1f\", value: 2 },\n          uDensity: { type: \"1f\", value: 1 },\n          uPixelJitter: { type: \"1f\", value: 0 },\n          uEnableRipples: { type: \"1i\", value: 1 },\n          uRippleSpeed: { type: \"1f\", value: 0.3 },\n          uRippleThickness: { type: \"1f\", value: 0.1 },\n          uRippleIntensity: { type: \"1f\", value: props.rippleIntensity ?? 1 },\n          uEdgeFade: { type: \"1f\", value: 0.5 },\n          uClickPos: { type: \"2fv\", value: ripple.positions },\n          uClickTimes: { type: \"1fv\", value: ripple.times },\n        },\n        onPointerDown: ({ gl, program, canvasX, canvasY, time }) => {\n          ripple.positions[ripple.index * 2] = canvasX;\n          ripple.positions[ripple.index * 2 + 1] = canvasY;\n          ripple.times[ripple.index] = time;\n          ripple.index = (ripple.index + 1) % MAX_RIPPLE_CLICKS;\n          const posLoc = gl.getUniformLocation(program, \"uClickPos\");\n          const timeLoc = gl.getUniformLocation(program, \"uClickTimes\");\n          if (posLoc) gl.uniform2fv(posLoc, new Float32Array(ripple.positions));\n          if (timeLoc) gl.uniform1fv(timeLoc, new Float32Array(ripple.times));\n        },\n      };\n    }\n    case \"dither\": {\n      const rgb = hexToRgb(props.color ?? \"#808080\");\n      return {\n        frag: DITHER_FRAG,\n        useMouse: true,\n        uniforms: {\n          waveSpeed: { type: \"1f\", value: props.speed ?? 0.05 },\n          waveFrequency: { type: \"1f\", value: 3 },\n          waveAmplitude: { type: \"1f\", value: 0.3 },\n          waveColor: { type: \"3f\", value: rgb },\n          enableMouseInteraction: { type: \"1i\", value: 1 },\n          mouseRadius: { type: \"1f\", value: 1 },\n          colorNum: { type: \"1f\", value: 4 },\n          pixelSize: { type: \"1f\", value: props.pixelSize ?? 2 },\n        },\n      };\n    }\n  }\n}\n\nfunction compileShader(gl: GLContext, type: number, source: string): WebGLShader | null {\n  const shader = gl.createShader(type);\n  if (!shader) return null;\n  gl.shaderSource(shader, source);\n  gl.compileShader(shader);\n  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n    console.warn(\"[webgl-background] shader compile error\", gl.getShaderInfoLog(shader));\n  }\n  return shader;\n}\n\nfunction applyUniform(gl: GLContext, loc: WebGLUniformLocation, spec: UniformSpec) {\n  switch (spec.type) {\n    case \"1f\":\n      gl.uniform1f(loc, spec.value);\n      break;\n    case \"1i\":\n      gl.uniform1i(loc, spec.value);\n      break;\n    case \"2f\":\n      gl.uniform2f(loc, spec.value[0], spec.value[1]);\n      break;\n    case \"3f\":\n      gl.uniform3f(loc, spec.value[0], spec.value[1], spec.value[2]);\n      break;\n    case \"1fv\":\n      gl.uniform1fv(loc, new Float32Array(spec.value));\n      break;\n    case \"2fv\":\n      gl.uniform2fv(loc, new Float32Array(spec.value));\n      break;\n    case \"3fv\":\n      gl.uniform3fv(loc, new Float32Array(spec.value));\n      break;\n  }\n}\n\nconst GL_CONTEXT_OPTIONS: WebGLContextAttributes = {\n  alpha: true,\n  premultipliedAlpha: true,\n  antialias: true,\n};\n\n/**\n * Runs a full-screen fragment shader inside `containerRef`, mirroring\n * motion-anything's `_fx/shaderbg.js`: WebGL1/WebGL2 auto-detection, the\n * uTime/uResolution/uMouse auto-uniforms, a devicePixelRatio-aware resize\n * loop, and a single static frame under reduced motion. Returns whether\n * a WebGL context could be created, so the caller can render a CSS fallback.\n */\nfunction useShaderCanvas(\n  containerRef: RefObject<HTMLDivElement | null>,\n  config: ShaderConfig,\n  reducedMotion: boolean,\n): boolean {\n  const [supported, setSupported] = useState(true);\n  const configRef = useRef(config);\n  configRef.current = config;\n  // uClickPos/uClickTimes (pixel-blast) mutate outside of React's render cycle as the user\n  // clicks; excluding them keeps an unrelated re-render from tearing down and rebuilding the\n  // WebGL context mid-interaction.\n  const configKey = `${config.frag}|${JSON.stringify(config.uniforms, (key, value) =>\n    key === \"uClickPos\" || key === \"uClickTimes\" ? undefined : value,\n  )}|${config.useMouse}|${config.timeScale ?? 1}`;\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: configKey is a synthetic dep that mirrors configRef.current, which the effect reads live\n  useEffect(() => {\n    const containerEl = containerRef.current;\n    if (!containerEl) return;\n    const container: HTMLDivElement = containerEl;\n    const { frag, uniforms, useMouse, timeScale = 1, onPointerDown } = configRef.current;\n\n    const isGL2 = /#version\\s+300/.test(frag);\n    const canvas = document.createElement(\"canvas\");\n    canvas.style.cssText = \"width:100%;height:100%;display:block\";\n    container.appendChild(canvas);\n\n    const gl: GLContext | null =\n      (isGL2 ? canvas.getContext(\"webgl2\", GL_CONTEXT_OPTIONS) : null) ??\n      canvas.getContext(\"webgl\", GL_CONTEXT_OPTIONS);\n\n    if (!gl) {\n      container.removeChild(canvas);\n      setSupported(false);\n      return;\n    }\n    const glCtx: GLContext = gl;\n\n    const usesGL2 =\n      typeof WebGL2RenderingContext !== \"undefined\" && glCtx instanceof WebGL2RenderingContext;\n    const vert = usesGL2 ? VERT_GL2 : VERT_GL1;\n\n    glCtx.clearColor(0, 0, 0, 0);\n    glCtx.enable(glCtx.BLEND);\n    glCtx.blendFunc(glCtx.ONE, glCtx.ONE_MINUS_SRC_ALPHA);\n\n    const program = glCtx.createProgram();\n    const vertShader = compileShader(glCtx, glCtx.VERTEX_SHADER, vert);\n    const fragShader = compileShader(glCtx, glCtx.FRAGMENT_SHADER, frag);\n    if (!program || !vertShader || !fragShader) {\n      container.removeChild(canvas);\n      setSupported(false);\n      return;\n    }\n    glCtx.attachShader(program, vertShader);\n    glCtx.attachShader(program, fragShader);\n    glCtx.linkProgram(program);\n    if (!glCtx.getProgramParameter(program, glCtx.LINK_STATUS)) {\n      console.warn(\"[webgl-background] program link error\", glCtx.getProgramInfoLog(program));\n      container.removeChild(canvas);\n      setSupported(false);\n      return;\n    }\n    // biome-ignore lint/correctness/useHookAtTopLevel: gl.useProgram is a WebGL API method, not a React hook\n    glCtx.useProgram(program);\n\n    const posBuffer = glCtx.createBuffer();\n    glCtx.bindBuffer(glCtx.ARRAY_BUFFER, posBuffer);\n    glCtx.bufferData(\n      glCtx.ARRAY_BUFFER,\n      new Float32Array([-1, -1, 3, -1, -1, 3]),\n      glCtx.STATIC_DRAW,\n    );\n    const posLoc = glCtx.getAttribLocation(program, \"position\");\n    glCtx.enableVertexAttribArray(posLoc);\n    glCtx.vertexAttribPointer(posLoc, 2, glCtx.FLOAT, false, 0, 0);\n\n    const uvLoc = glCtx.getAttribLocation(program, \"uv\");\n    if (uvLoc >= 0) {\n      const uvBuffer = glCtx.createBuffer();\n      glCtx.bindBuffer(glCtx.ARRAY_BUFFER, uvBuffer);\n      glCtx.bufferData(\n        glCtx.ARRAY_BUFFER,\n        new Float32Array([0, 0, 2, 0, 0, 2]),\n        glCtx.STATIC_DRAW,\n      );\n      glCtx.enableVertexAttribArray(uvLoc);\n      glCtx.vertexAttribPointer(uvLoc, 2, glCtx.FLOAT, false, 0, 0);\n    }\n\n    const uTimeLoc = glCtx.getUniformLocation(program, \"uTime\");\n    const iTimeLoc = glCtx.getUniformLocation(program, \"iTime\");\n    const uResLoc = glCtx.getUniformLocation(program, \"uResolution\");\n    const iResLoc = glCtx.getUniformLocation(program, \"iResolution\");\n    const uMouseLoc = useMouse ? glCtx.getUniformLocation(program, \"uMouse\") : null;\n    const resolutionIsVec3 = /vec3\\s+(uResolution|iResolution)/.test(frag);\n\n    for (const [name, spec] of Object.entries(uniforms)) {\n      const loc = glCtx.getUniformLocation(program, name);\n      if (loc) applyUniform(glCtx, loc, spec);\n    }\n\n    let mouse: [number, number] = [0.5, 0.5];\n    function handlePointerMove(event: PointerEvent) {\n      const rect = container.getBoundingClientRect();\n      mouse = [\n        (event.clientX - rect.left) / rect.width,\n        1 - (event.clientY - rect.top) / rect.height,\n      ];\n    }\n    if (useMouse) {\n      container.addEventListener(\"pointermove\", handlePointerMove, { passive: true });\n    }\n\n    function handlePointerDown(event: PointerEvent) {\n      if (!onPointerDown) return;\n      const rect = container.getBoundingClientRect();\n      const scaleX = canvas.width / rect.width;\n      const scaleY = canvas.height / rect.height;\n      onPointerDown({\n        gl: glCtx,\n        program,\n        canvasX: (event.clientX - rect.left) * scaleX,\n        canvasY: (rect.height - (event.clientY - rect.top)) * scaleY,\n        time: currentTime,\n      });\n    }\n    if (onPointerDown) {\n      container.addEventListener(\"pointerdown\", handlePointerDown, { passive: true });\n    }\n\n    function resize() {\n      const width = Math.max(1, container.clientWidth);\n      const height = Math.max(1, container.clientHeight);\n      const dpr = Math.min(window.devicePixelRatio || 1, 2);\n      canvas.width = Math.round(width * dpr);\n      canvas.height = Math.round(height * dpr);\n      glCtx.viewport(0, 0, canvas.width, canvas.height);\n    }\n    const resizeObserver = new ResizeObserver(resize);\n    resizeObserver.observe(container);\n    resize();\n\n    let rafId = 0;\n    let currentTime = reducedMotion ? 2 : 0;\n    function frame(t: number) {\n      const time = reducedMotion ? 2 : t * 0.001 * timeScale;\n      currentTime = time;\n      if (uTimeLoc) glCtx.uniform1f(uTimeLoc, time);\n      if (iTimeLoc) glCtx.uniform1f(iTimeLoc, time);\n      if (uResLoc) {\n        if (resolutionIsVec3) glCtx.uniform3f(uResLoc, canvas.width, canvas.height, 1);\n        else glCtx.uniform2f(uResLoc, canvas.width, canvas.height);\n      }\n      if (iResLoc) {\n        if (resolutionIsVec3) glCtx.uniform3f(iResLoc, canvas.width, canvas.height, 1);\n        else glCtx.uniform2f(iResLoc, canvas.width, canvas.height);\n      }\n      if (uMouseLoc) glCtx.uniform2f(uMouseLoc, mouse[0], mouse[1]);\n      glCtx.clear(glCtx.COLOR_BUFFER_BIT);\n      glCtx.drawArrays(glCtx.TRIANGLES, 0, 3);\n      if (!reducedMotion) rafId = requestAnimationFrame(frame);\n    }\n\n    if (reducedMotion) {\n      frame(2000);\n    } else {\n      rafId = requestAnimationFrame(frame);\n    }\n\n    setSupported(true);\n\n    return () => {\n      if (rafId) cancelAnimationFrame(rafId);\n      resizeObserver.disconnect();\n      if (useMouse) container.removeEventListener(\"pointermove\", handlePointerMove);\n      if (onPointerDown) container.removeEventListener(\"pointerdown\", handlePointerDown);\n      glCtx.getExtension(\"WEBGL_lose_context\")?.loseContext();\n      if (canvas.parentNode === container) container.removeChild(canvas);\n    };\n  }, [containerRef, configKey, reducedMotion]);\n\n  return supported;\n}\n\n/* ------------------------------- component -------------------------------- */\n\n/**\n * Full-screen WebGL fragment-shader background (aurora / silk / plasma /\n * light-rays / pixel-blast / dither). Ported from motion-anything's\n * dependency-free shader runner. Renders a single static frame under\n * reduced motion, and a CSS gradient fallback when no WebGL context is\n * available. pixel-blast additionally responds to clicks with a ripple.\n */\nexport function WebGLBackground(props: WebGLBackgroundProps) {\n  const { className, variant } = props;\n  const containerRef = useRef<HTMLDivElement>(null);\n  const reducedMotion = useReducedMotion() ?? false;\n  const rippleRef = useRef<RippleState | null>(null);\n  if (!rippleRef.current) rippleRef.current = createRippleState();\n  const config = buildShaderConfig(props, rippleRef.current);\n  const supported = useShaderCanvas(containerRef, config, reducedMotion);\n\n  return (\n    <div ref={containerRef} className={cn(\"relative h-full w-full overflow-hidden\", className)}>\n      {!supported && (\n        <div\n          className=\"absolute inset-0\"\n          style={{ background: FALLBACK_GRADIENT[variant] }}\n          aria-hidden=\"true\"\n        />\n      )}\n    </div>\n  );\n}\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}