{
  "name": "closing-plasma",
  "type": "registry:ui",
  "dependencies": [
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [],
  "description": "Premium plasma background with customizable motion, palette, and interaction.",
  "files": [
    {
      "path": "components/ui/closing-plasma.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useMemo, useRef } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst VERTEX_SHADER = `\nattribute vec2 position;\nvoid main() {\n  gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst FRAGMENT_SHADER = `\nprecision highp float;\n\nuniform vec2 u_res;\nuniform float u_time;\nuniform vec2 u_mouse;\nuniform float u_isDark;\nuniform float u_speed;\nuniform float u_turbulence;\nuniform float u_mouseInfluence;\nuniform float u_grain;\nuniform float u_sparkle;\nuniform float u_vignette;\nuniform float u_opacity;\n\nuniform vec3 u_darkA;\nuniform vec3 u_darkB;\nuniform vec3 u_darkC;\nuniform vec3 u_lightA;\nuniform vec3 u_lightB;\nuniform vec3 u_lightC;\n\nvec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }\nvec2 mod289(vec2 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }\nvec3 permute(vec3 x) { return mod289(((x * 34.0) + 1.0) * x); }\n\nfloat snoise(vec2 v) {\n  const vec4 C = vec4(0.211324865405187, 0.366025403784439,\n                     -0.577350269189626, 0.024390243902439);\n  vec2 i = floor(v + dot(v, C.yy));\n  vec2 x0 = v - i + dot(i, C.xx);\n  vec2 i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);\n  vec4 x12 = x0.xyxy + C.xxzz;\n  x12.xy -= i1;\n  i = mod289(i);\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);\n  m = m * m;\n  m = m * m;\n  vec3 x = 2.0 * fract(p * C.www) - 1.0;\n  vec3 h = abs(x) - 0.5;\n  vec3 ox = floor(x + 0.5);\n  vec3 a0 = x - ox;\n  m *= 1.79284291400159 - 0.85373472095314 * (a0 * a0 + h * h);\n  vec3 g;\n  g.x = a0.x * x0.x + h.x * x0.y;\n  g.yz = a0.yz * x12.xz + h.yz * x12.yw;\n  return 130.0 * dot(m, g);\n}\n\nfloat fbm(vec2 p, float turbulence) {\n  float total = 0.0;\n  float amp = 0.5;\n  float freq = 1.0;\n  mat2 rot = mat2(cos(0.45), sin(0.45), -sin(0.45), cos(0.45));\n  for (int i = 0; i < 5; i++) {\n    total += snoise(p * freq) * amp;\n    p = rot * p;\n    freq *= mix(1.85, 2.35, clamp(turbulence, 0.0, 2.0) * 0.5);\n    amp *= 0.5;\n  }\n  return total;\n}\n\nvoid main() {\n  vec2 uv = gl_FragCoord.xy / u_res;\n  float aspect = u_res.x / max(u_res.y, 1.0);\n  vec2 p = (uv - 0.5) * vec2(aspect, 1.0);\n  float t = u_time * (0.15 * u_speed);\n\n  vec2 mouse = (u_mouse - 0.5) * vec2(aspect, 1.0);\n  float dMouse = length(p - mouse);\n  p += (mouse - p) * 0.02 * u_mouseInfluence * smoothstep(0.45, 0.0, dMouse);\n\n  vec2 flow = vec2(\n    fbm(p + vec2(t * 0.2, t * 0.1), u_turbulence),\n    fbm(p + vec2(-t * 0.1, t * 0.3), u_turbulence)\n  );\n\n  float n = fbm(p * 2.0 + flow * 1.45, u_turbulence);\n  float ridges = 1.0 - abs(snoise(p * 4.0 + n) * 2.0);\n  ridges = pow(ridges, 3.0);\n\n  vec3 colorA = mix(u_lightA, u_darkA, u_isDark);\n  vec3 colorB = mix(u_lightB, u_darkB, u_isDark);\n  vec3 colorC = mix(u_lightC, u_darkC, u_isDark);\n\n  vec3 col = mix(colorA, colorB, smoothstep(-0.5, 0.5, n));\n  col = mix(col, colorC, smoothstep(0.25, 1.0, n * 0.52 + ridges * 0.48));\n\n  float sparkle = pow(max(0.0, snoise(gl_FragCoord.xy * 0.2 + t * 2.0)), 18.0) * 0.5 * u_sparkle;\n  vec3 sparkleColor = mix(vec3(0.56, 0.58, 0.72), vec3(0.8, 0.9, 1.0), u_isDark);\n  col += sparkleColor * sparkle;\n\n  float vigDark = 1.0 - smoothstep(0.5, mix(1.8, 1.55, u_isDark), length(p));\n  col = mix(col, col * vigDark, u_isDark * u_vignette);\n  float vigLight = 1.0 - smoothstep(0.4, 1.45, length(p));\n  col = mix(mix(vec3(1.0), col, vigLight), col, u_isDark);\n\n  float grain = (fract(sin(dot(gl_FragCoord.xy + t * 50.0, vec2(12.9898, 78.233))) * 43758.5453) - 0.5) * (0.06 * u_grain);\n  col += grain;\n\n  gl_FragColor = vec4(clamp(col, 0.0, 1.0), u_opacity);\n}\n`;\n\nconst HEX_COLOR_REGEX = /^#?[0-9a-fA-F]{6}$/;\n\nfunction sanitizeHexColor(value: string, fallback: string) {\n  const trimmed = value.trim();\n  if (!HEX_COLOR_REGEX.test(trimmed)) {\n    return fallback;\n  }\n  return trimmed.startsWith(\"#\") ? trimmed : `#${trimmed}`;\n}\n\nfunction hexToRgb01(hex: string, fallback: string): [number, number, number] {\n  const normalized = sanitizeHexColor(hex, fallback).replace(\"#\", \"\");\n  const r = parseInt(normalized.slice(0, 2), 16) / 255;\n  const g = parseInt(normalized.slice(2, 4), 16) / 255;\n  const b = parseInt(normalized.slice(4, 6), 16) / 255;\n  return [r, g, b];\n}\n\nconst DARK_A = \"#0d0d14\";\nconst DARK_B = \"#1f2540\";\nconst DARK_C = \"#4a6191\";\nconst LIGHT_A = \"#f0f2f7\";\nconst LIGHT_B = \"#d7dceb\";\nconst LIGHT_C = \"#bcc5e0\";\n\nexport interface ClosingPlasmaProps extends React.HTMLAttributes<HTMLDivElement> {\n  themeMode?: \"auto\" | \"light\" | \"dark\";\n  speed?: number;\n  turbulence?: number;\n  mouseInfluence?: number;\n  grain?: number;\n  sparkle?: number;\n  vignette?: number;\n  opacity?: number;\n  interactive?: boolean;\n  darkColorA?: string;\n  darkColorB?: string;\n  darkColorC?: string;\n  lightColorA?: string;\n  lightColorB?: string;\n  lightColorC?: string;\n  children?: React.ReactNode;\n}\n\nexport function ClosingPlasma({\n  themeMode = \"auto\",\n  speed = 1,\n  turbulence = 1,\n  mouseInfluence = 1,\n  grain = 1,\n  sparkle = 1,\n  vignette = 1,\n  opacity = 1,\n  interactive = true,\n  darkColorA = DARK_A,\n  darkColorB = DARK_B,\n  darkColorC = DARK_C,\n  lightColorA = LIGHT_A,\n  lightColorB = LIGHT_B,\n  lightColorC = LIGHT_C,\n  className,\n  children,\n  style,\n  ...props\n}: ClosingPlasmaProps) {\n  const containerRef = useRef<HTMLDivElement | null>(null);\n  const canvasRef = useRef<HTMLCanvasElement | null>(null);\n  const mouseRef = useRef({ x: 0.5, y: 0.5 });\n  const targetMouseRef = useRef({ x: 0.5, y: 0.5 });\n  const isDarkRef = useRef(0);\n\n  const settings = useMemo(\n    () => ({\n      speed,\n      turbulence,\n      mouseInfluence,\n      grain,\n      sparkle,\n      vignette,\n      opacity,\n      interactive,\n      darkColorA,\n      darkColorB,\n      darkColorC,\n      lightColorA,\n      lightColorB,\n      lightColorC,\n    }),\n    [\n      speed,\n      turbulence,\n      mouseInfluence,\n      grain,\n      sparkle,\n      vignette,\n      opacity,\n      interactive,\n      darkColorA,\n      darkColorB,\n      darkColorC,\n      lightColorA,\n      lightColorB,\n      lightColorC,\n    ],\n  );\n\n  useEffect(() => {\n    const computeTheme = () => {\n      if (themeMode === \"dark\") {\n        isDarkRef.current = 1;\n        return;\n      }\n      if (themeMode === \"light\") {\n        isDarkRef.current = 0;\n        return;\n      }\n      isDarkRef.current = document.documentElement.classList.contains(\"dark\") ? 1 : 0;\n    };\n\n    computeTheme();\n\n    if (themeMode !== \"auto\") {\n      return;\n    }\n\n    const observer = new MutationObserver(computeTheme);\n    observer.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\"],\n    });\n\n    return () => observer.disconnect();\n  }, [themeMode]);\n\n  useEffect(() => {\n    const container = containerRef.current;\n    const canvas = canvasRef.current;\n    if (!container || !canvas) {\n      return;\n    }\n\n    const handlePointerMove = (event: PointerEvent) => {\n      if (!settings.interactive) {\n        return;\n      }\n      const rect = container.getBoundingClientRect();\n      targetMouseRef.current = {\n        x: (event.clientX - rect.left) / rect.width,\n        y: 1 - (event.clientY - rect.top) / rect.height,\n      };\n    };\n\n    const handlePointerLeave = () => {\n      targetMouseRef.current = { x: 0.5, y: 0.5 };\n    };\n\n    container.addEventListener(\"pointermove\", handlePointerMove);\n    container.addEventListener(\"pointerleave\", handlePointerLeave);\n\n    const gl = canvas.getContext(\"webgl\", { antialias: false, alpha: true });\n    if (!gl) {\n      return () => {\n        container.removeEventListener(\"pointermove\", handlePointerMove);\n        container.removeEventListener(\"pointerleave\", handlePointerLeave);\n      };\n    }\n\n    const compileShader = (type: number, source: string) => {\n      const shader = gl.createShader(type);\n      if (!shader) {\n        return null;\n      }\n      gl.shaderSource(shader, source);\n      gl.compileShader(shader);\n      if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n        gl.deleteShader(shader);\n        return null;\n      }\n      return shader;\n    };\n\n    const vertexShader = compileShader(gl.VERTEX_SHADER, VERTEX_SHADER);\n    const fragmentShader = compileShader(gl.FRAGMENT_SHADER, FRAGMENT_SHADER);\n    if (!vertexShader || !fragmentShader) {\n      return;\n    }\n\n    const program = gl.createProgram();\n    if (!program) {\n      gl.deleteShader(vertexShader);\n      gl.deleteShader(fragmentShader);\n      return;\n    }\n\n    gl.attachShader(program, vertexShader);\n    gl.attachShader(program, fragmentShader);\n    gl.linkProgram(program);\n    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n      gl.deleteProgram(program);\n      gl.deleteShader(vertexShader);\n      gl.deleteShader(fragmentShader);\n      return;\n    }\n\n    gl.useProgram(program);\n\n    const position = gl.getAttribLocation(program, \"position\");\n    const buffer = gl.createBuffer();\n    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n    gl.bufferData(\n      gl.ARRAY_BUFFER,\n      new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),\n      gl.STATIC_DRAW,\n    );\n    gl.enableVertexAttribArray(position);\n    gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);\n\n    const uRes = gl.getUniformLocation(program, \"u_res\");\n    const uTime = gl.getUniformLocation(program, \"u_time\");\n    const uMouse = gl.getUniformLocation(program, \"u_mouse\");\n    const uIsDark = gl.getUniformLocation(program, \"u_isDark\");\n    const uSpeed = gl.getUniformLocation(program, \"u_speed\");\n    const uTurbulence = gl.getUniformLocation(program, \"u_turbulence\");\n    const uMouseInfluence = gl.getUniformLocation(program, \"u_mouseInfluence\");\n    const uGrain = gl.getUniformLocation(program, \"u_grain\");\n    const uSparkle = gl.getUniformLocation(program, \"u_sparkle\");\n    const uVignette = gl.getUniformLocation(program, \"u_vignette\");\n    const uOpacity = gl.getUniformLocation(program, \"u_opacity\");\n    const uDarkA = gl.getUniformLocation(program, \"u_darkA\");\n    const uDarkB = gl.getUniformLocation(program, \"u_darkB\");\n    const uDarkC = gl.getUniformLocation(program, \"u_darkC\");\n    const uLightA = gl.getUniformLocation(program, \"u_lightA\");\n    const uLightB = gl.getUniformLocation(program, \"u_lightB\");\n    const uLightC = gl.getUniformLocation(program, \"u_lightC\");\n\n    if (\n      !uRes ||\n      !uTime ||\n      !uMouse ||\n      !uIsDark ||\n      !uSpeed ||\n      !uTurbulence ||\n      !uMouseInfluence ||\n      !uGrain ||\n      !uSparkle ||\n      !uVignette ||\n      !uOpacity ||\n      !uDarkA ||\n      !uDarkB ||\n      !uDarkC ||\n      !uLightA ||\n      !uLightB ||\n      !uLightC\n    ) {\n      gl.deleteBuffer(buffer);\n      gl.deleteProgram(program);\n      gl.deleteShader(vertexShader);\n      gl.deleteShader(fragmentShader);\n      return;\n    }\n\n    const resize = () => {\n      const dpr = Math.min(window.devicePixelRatio || 1, 1.75);\n      const { width, height } = container.getBoundingClientRect();\n      canvas.width = Math.max(1, Math.floor(width * dpr));\n      canvas.height = Math.max(1, Math.floor(height * dpr));\n      gl.viewport(0, 0, canvas.width, canvas.height);\n      gl.uniform2f(uRes, canvas.width, canvas.height);\n    };\n\n    resize();\n    const resizeObserver = new ResizeObserver(resize);\n    resizeObserver.observe(container);\n\n    const darkA = hexToRgb01(settings.darkColorA, DARK_A);\n    const darkB = hexToRgb01(settings.darkColorB, DARK_B);\n    const darkC = hexToRgb01(settings.darkColorC, DARK_C);\n    const lightA = hexToRgb01(settings.lightColorA, LIGHT_A);\n    const lightB = hexToRgb01(settings.lightColorB, LIGHT_B);\n    const lightC = hexToRgb01(settings.lightColorC, LIGHT_C);\n\n    gl.uniform3f(uDarkA, darkA[0], darkA[1], darkA[2]);\n    gl.uniform3f(uDarkB, darkB[0], darkB[1], darkB[2]);\n    gl.uniform3f(uDarkC, darkC[0], darkC[1], darkC[2]);\n    gl.uniform3f(uLightA, lightA[0], lightA[1], lightA[2]);\n    gl.uniform3f(uLightB, lightB[0], lightB[1], lightB[2]);\n    gl.uniform3f(uLightC, lightC[0], lightC[1], lightC[2]);\n\n    let rafId = 0;\n    const start = performance.now();\n\n    const render = (now: number) => {\n      const elapsed = (now - start) / 1000;\n      mouseRef.current.x += (targetMouseRef.current.x - mouseRef.current.x) * 0.05;\n      mouseRef.current.y += (targetMouseRef.current.y - mouseRef.current.y) * 0.05;\n\n      gl.uniform1f(uTime, elapsed);\n      gl.uniform2f(uMouse, mouseRef.current.x, mouseRef.current.y);\n      gl.uniform1f(uIsDark, isDarkRef.current);\n      gl.uniform1f(uSpeed, settings.speed);\n      gl.uniform1f(uTurbulence, settings.turbulence);\n      gl.uniform1f(uMouseInfluence, settings.mouseInfluence);\n      gl.uniform1f(uGrain, settings.grain);\n      gl.uniform1f(uSparkle, settings.sparkle);\n      gl.uniform1f(uVignette, settings.vignette);\n      gl.uniform1f(uOpacity, settings.opacity);\n      gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);\n      rafId = requestAnimationFrame(render);\n    };\n\n    rafId = requestAnimationFrame(render);\n\n    return () => {\n      container.removeEventListener(\"pointermove\", handlePointerMove);\n      container.removeEventListener(\"pointerleave\", handlePointerLeave);\n      cancelAnimationFrame(rafId);\n      resizeObserver.disconnect();\n      gl.deleteBuffer(buffer);\n      gl.deleteProgram(program);\n      gl.deleteShader(vertexShader);\n      gl.deleteShader(fragmentShader);\n    };\n  }, [settings]);\n\n  return (\n    <div\n      ref={containerRef}\n      className={cn(\"relative h-full w-full overflow-hidden\", className)}\n      style={style}\n      {...props}\n    >\n      <canvas\n        ref={canvasRef}\n        aria-hidden=\"true\"\n        className=\"absolute inset-0 h-full w-full\"\n        style={{ width: \"100%\", height: \"100%\", display: \"block\" }}\n      />\n      {children && <div className=\"relative z-10 h-full w-full\">{children}</div>}\n    </div>\n  );\n}\n\nexport default ClosingPlasma;\n",
      "type": "registry:ui"
    }
  ]
}