{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "thermal-flow",
  "title": "Thermal Flow",
  "description": "A smooth, rich thermal-gradient background rendered on the GPU, with seven themes.",
  "dependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/clover-ui/thermal-flow.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport type ThermalFlowTheme =\n  \"thermal\" | \"aurora\" | \"sunset\" | \"ocean\" | \"ember\" | \"candy\" | \"mono\";\n\nexport const THERMAL_FLOW_THEMES: Record<ThermalFlowTheme, string[]> = {\n  thermal: [\"#04040c\", \"#0b1fd6\", \"#2a7cff\", \"#59b8f5\", \"#f6e04a\", \"#f79a20\", \"#f0401a\"],\n  aurora: [\"#02030a\", \"#0b2a5b\", \"#0aa3a3\", \"#3df2a7\", \"#c5f56b\", \"#ff8bd0\", \"#b14dff\"],\n  sunset: [\"#0a0414\", \"#3a0ca3\", \"#b5179e\", \"#f72585\", \"#ff7a3d\", \"#ffd166\", \"#fff1b8\"],\n  ocean: [\"#020617\", \"#0b2a6f\", \"#0e6bd8\", \"#22d3ee\", \"#a5f3fc\", \"#e0fbff\", \"#ffffff\"],\n  ember: [\"#050203\", \"#3b0a0a\", \"#8f1d0f\", \"#e8420f\", \"#ff9a1f\", \"#ffd36b\", \"#fff3c4\"],\n  candy: [\"#12061f\", \"#5b21b6\", \"#a855f7\", \"#f472b6\", \"#fda4af\", \"#fde68a\", \"#a7f3d0\"],\n  mono: [\"#050505\", \"#1a1a1a\", \"#4a4a4a\", \"#8a8a8a\", \"#c8c8c8\", \"#f2f2f2\", \"#ffffff\"],\n};\n\nexport interface ThermalFlowProps {\n  theme?: ThermalFlowTheme;\n  colors?: string[];\n  bands?: number;\n  scale?: number;\n  warp?: number;\n  softness?: number;\n  grain?: number;\n  animate?: boolean;\n  speed?: number;\n  resolution?: number;\n  radius?: string;\n  backgroundColor?: string;\n  style?: React.CSSProperties;\n  className?: string;\n}\n\nconst MAX_STOPS = 8;\n\nfunction parseColor(input: string): [number, number, number] {\n  const s = input.trim();\n  if (s.startsWith(\"#\")) {\n    let c = s.slice(1);\n    if (c.length === 3 || c.length === 4)\n      c = c\n        .split(\"\")\n        .map((x) => x + x)\n        .join(\"\");\n    if (c.length >= 6) {\n      return [\n        parseInt(c.slice(0, 2), 16) / 255,\n        parseInt(c.slice(2, 4), 16) / 255,\n        parseInt(c.slice(4, 6), 16) / 255,\n      ];\n    }\n  }\n  const m = s.match(/rgba?\\(([^)]+)\\)/i);\n  if (m) {\n    const [r, g, b] = m[1].split(\",\").map((x) => parseFloat(x));\n    return [r / 255, g / 255, b / 255];\n  }\n  return [0, 0, 0];\n}\n\nfunction buildPalette(colors: string[]) {\n  const rgb = colors.slice(0, MAX_STOPS).map(parseColor);\n  const data = new Float32Array(MAX_STOPS * 3);\n  for (let i = 0; i < MAX_STOPS; i++) {\n    const c = rgb[Math.min(i, rgb.length - 1)];\n    data.set(c, i * 3);\n  }\n  return { data, count: rgb.length };\n}\n\ntype Live = {\n  palette: { data: Float32Array; count: number };\n  bands: number;\n  scale: number;\n  warp: number;\n  softness: number;\n  grain: number;\n  animate: boolean;\n  speed: number;\n  resolution: number;\n};\n\nconst VERTEX = `\nattribute vec2 a_pos;\nvoid main() { gl_Position = vec4(a_pos, 0.0, 1.0); }\n`;\n\nconst FRAGMENT = `\n#extension GL_OES_standard_derivatives : enable\n#ifdef GL_FRAGMENT_PRECISION_HIGH\nprecision highp float;\n#else\nprecision mediump float;\n#endif\n\nuniform vec2 u_res;\nuniform float u_time;\nuniform vec3 u_pal[8];\nuniform float u_n;\nuniform float u_bands;\nuniform float u_scale;\nuniform float u_warp;\nuniform float u_soft;\nuniform float u_grain;\n\nvec3 permute(vec3 x) { return mod(((x * 34.0) + 1.0) * x, 289.0); }\n\nfloat snoise(vec2 v) {\n  const vec4 C = vec4(0.211324865405187, 0.366025403784439, -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 = 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);\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) {\n  return 0.6 * snoise(p) + 0.25 * snoise(p * 2.0 + vec2(17.1, 9.3));\n}\n\nvec3 pal(float t) {\n  t = clamp(t, 0.0, 1.0) * (u_n - 1.0);\n  vec3 c = u_pal[0];\n  for (int i = 0; i < 7; i++) {\n    float k = clamp(t - float(i), 0.0, 1.0);\n    k = k * k * (3.0 - 2.0 * k);\n    c = mix(c, u_pal[i + 1], k);\n  }\n  return c;\n}\n\nvoid main() {\n  vec2 uv = gl_FragCoord.xy / u_res;\n  vec2 p = (uv - 0.5) * vec2(u_res.x / u_res.y, 1.0) * 0.6 / u_scale;\n  float t = u_time * 0.12;\n\n  vec2 q = vec2(fbm(p + vec2(t * 0.5, 0.0)), fbm(p + vec2(5.2, 1.3 - t * 0.4)));\n  float f = fbm(p + u_warp * 0.6 * q);\n\n  float v = (f * 0.85 + 0.5) * u_bands + u_time * 0.03;\n  float fr = fract(v);\n\n#ifdef GL_OES_standard_derivatives\n  float fw = fwidth(v);\n#else\n  float fw = 0.004;\n#endif\n\n  float w = u_soft + fw * 1.5;\n  float d = min(fr, 1.0 - fr);\n  vec3 c = pal(fr);\n  vec3 across = fr < 0.5 ? pal(1.0) : pal(0.0);\n  float k = clamp(d / w, 0.0, 1.0);\n  c = mix(c, across, 0.5 * (1.0 - k * k * (3.0 - 2.0 * k)));\n\n  float n = fract(sin(dot(gl_FragCoord.xy, vec2(12.9898, 78.233))) * 43758.5453) - 0.5;\n  c += n * u_grain * 0.06;\n\n  gl_FragColor = vec4(clamp(c, 0.0, 1.0), 1.0);\n}\n`;\n\nfunction ThermalFlow({\n  theme = \"thermal\",\n  colors,\n  bands = 1,\n  scale = 1,\n  warp = 1,\n  softness = 0.005,\n  grain = 0.35,\n  animate = true,\n  speed = 1,\n  resolution = 0.75,\n  radius,\n  backgroundColor,\n  style,\n  className,\n}: ThermalFlowProps) {\n  const hostRef = React.useRef<HTMLDivElement>(null);\n  const canvasRef = React.useRef<HTMLCanvasElement>(null);\n  const syncRef = React.useRef<(() => void) | null>(null);\n  const [failed, setFailed] = React.useState(false);\n  const [nonce, setNonce] = React.useState(0);\n\n  const list = colors && colors.length >= 2 ? colors : THERMAL_FLOW_THEMES[theme];\n  const listKey = JSON.stringify(list);\n  const palette = React.useMemo(() => buildPalette(JSON.parse(listKey)), [listKey]);\n\n  const live = React.useRef<Live>({\n    palette,\n    bands,\n    scale,\n    warp,\n    softness,\n    grain,\n    animate,\n    speed,\n    resolution,\n  });\n\n  React.useEffect(() => {\n    live.current = { palette, bands, scale, warp, softness, grain, animate, speed, resolution };\n    syncRef.current?.();\n  });\n\n  React.useEffect(() => {\n    const canvas = canvasRef.current;\n    const host = hostRef.current;\n    if (!canvas || !host) return;\n\n    const gl = canvas.getContext(\"webgl\", {\n      alpha: false,\n      antialias: false,\n      depth: false,\n      stencil: false,\n      powerPreference: \"high-performance\",\n    });\n    if (!gl) {\n      setFailed(true);\n      return;\n    }\n    gl.getExtension(\"OES_standard_derivatives\");\n\n    const compile = (type: number, src: string) => {\n      const shader = gl.createShader(type)!;\n      gl.shaderSource(shader, src);\n      gl.compileShader(shader);\n      if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n        console.warn(\"ThermalFlow shader:\", gl.getShaderInfoLog(shader));\n        return null;\n      }\n      return shader;\n    };\n\n    const vs = compile(gl.VERTEX_SHADER, VERTEX);\n    const fs = compile(gl.FRAGMENT_SHADER, FRAGMENT);\n    const program = gl.createProgram()!;\n    if (!vs || !fs) {\n      setFailed(true);\n      return;\n    }\n    gl.attachShader(program, vs);\n    gl.attachShader(program, fs);\n    gl.linkProgram(program);\n    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n      console.warn(\"ThermalFlow link:\", gl.getProgramInfoLog(program));\n      setFailed(true);\n      return;\n    }\n    gl.useProgram(program);\n\n    const buffer = gl.createBuffer();\n    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);\n    const aPos = gl.getAttribLocation(program, \"a_pos\");\n    gl.enableVertexAttribArray(aPos);\n    gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);\n\n    const u = (name: string) => gl.getUniformLocation(program, name);\n    const loc = {\n      res: u(\"u_res\"),\n      time: u(\"u_time\"),\n      pal: u(\"u_pal[0]\"),\n      n: u(\"u_n\"),\n      bands: u(\"u_bands\"),\n      scale: u(\"u_scale\"),\n      warp: u(\"u_warp\"),\n      soft: u(\"u_soft\"),\n      grain: u(\"u_grain\"),\n    };\n\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    let raf = 0;\n    let visible = true;\n    let last = 0;\n    let time = Math.random() * 50;\n\n    const resize = () => {\n      const dpr = Math.min(window.devicePixelRatio || 1, 1.5);\n      const res = Math.min(1, Math.max(0.25, live.current.resolution));\n      const w = Math.max(1, Math.round(host.clientWidth * dpr * res));\n      const h = Math.max(1, Math.round(host.clientHeight * dpr * res));\n      if (canvas.width !== w || canvas.height !== h) {\n        canvas.width = w;\n        canvas.height = h;\n      }\n    };\n\n    const draw = () => {\n      const L = live.current;\n      gl.viewport(0, 0, canvas.width, canvas.height);\n      gl.uniform2f(loc.res, canvas.width, canvas.height);\n      gl.uniform1f(loc.time, time);\n      gl.uniform3fv(loc.pal, L.palette.data);\n      gl.uniform1f(loc.n, L.palette.count);\n      gl.uniform1f(loc.bands, L.bands);\n      gl.uniform1f(loc.scale, Math.max(0.1, L.scale));\n      gl.uniform1f(loc.warp, L.warp);\n      gl.uniform1f(loc.soft, L.softness);\n      gl.uniform1f(loc.grain, L.grain);\n      gl.drawArrays(gl.TRIANGLES, 0, 3);\n    };\n\n    const tick = (now: number) => {\n      raf = 0;\n      const L = live.current;\n      const moving = L.animate && L.speed !== 0 && !reduced;\n      const dt = last ? Math.min((now - last) / 1000, 0.1) : 0;\n      last = now;\n      if (moving) time += dt * L.speed;\n      draw();\n      if (moving && visible && !document.hidden) raf = requestAnimationFrame(tick);\n    };\n\n    const schedule = () => {\n      if (raf) return;\n      last = 0;\n      raf = requestAnimationFrame(tick);\n    };\n\n    syncRef.current = () => {\n      resize();\n      schedule();\n    };\n\n    resize();\n    schedule();\n\n    const ro = new ResizeObserver(() => syncRef.current?.());\n    ro.observe(host);\n\n    const io = new IntersectionObserver(([entry]) => {\n      visible = entry.isIntersecting;\n      if (visible) schedule();\n    });\n    io.observe(host);\n\n    const onVisibility = () => {\n      if (!document.hidden) schedule();\n    };\n    document.addEventListener(\"visibilitychange\", onVisibility);\n\n    const onLost = (e: Event) => {\n      e.preventDefault();\n      if (raf) cancelAnimationFrame(raf);\n      raf = 0;\n    };\n    const onRestored = () => setNonce((n) => n + 1);\n    canvas.addEventListener(\"webglcontextlost\", onLost);\n    canvas.addEventListener(\"webglcontextrestored\", onRestored);\n\n    return () => {\n      if (raf) cancelAnimationFrame(raf);\n      syncRef.current = null;\n      ro.disconnect();\n      io.disconnect();\n      document.removeEventListener(\"visibilitychange\", onVisibility);\n      canvas.removeEventListener(\"webglcontextlost\", onLost);\n      canvas.removeEventListener(\"webglcontextrestored\", onRestored);\n      gl.deleteBuffer(buffer);\n      gl.deleteProgram(program);\n      gl.deleteShader(vs);\n      gl.deleteShader(fs);\n    };\n  }, [nonce]);\n\n  const first = list[0];\n  const fallbackGradient = `linear-gradient(135deg, ${list.join(\", \")})`;\n\n  return (\n    <div\n      ref={hostRef}\n      aria-hidden=\"true\"\n      className={cn(\"pointer-events-none absolute inset-0 overflow-hidden\", className)}\n      style={{\n        borderRadius: radius,\n        backgroundColor: backgroundColor ?? first,\n        backgroundImage: failed ? fallbackGradient : undefined,\n        ...style,\n      }}\n    >\n      {!failed && <canvas ref={canvasRef} className=\"block h-full w-full\" />}\n    </div>\n  );\n}\n\nThermalFlow.displayName = \"ThermalFlow\";\n\nexport { ThermalFlow };\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}