{ "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 {\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(null);\n const canvasRef = useRef(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 \n \n {children &&
{children}
}\n \n );\n}\n\nexport default ClosingPlasma;\n", "type": "registry:ui" } ] }