{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "liquid-chrome",
  "type": "registry:component",
  "title": "Liquid Chrome",
  "description": "A chrome surface that ripples and reflects a luminous environment.",
  "author": "Marcelo Treviño Velázquez <https://furin.dev>",
  "dependencies": [],
  "categories": [
    "Backgrounds"
  ],
  "docs": "Installed in components/furin/liquid-chrome/. Docs, live controls and props table: https://furin.dev/components/liquid-chrome/",
  "files": [
    {
      "path": "registry/liquid-chrome/LiquidChrome.tsx",
      "content": "\"use client\";\nimport { useEffect, useRef } from \"react\";\nimport type { CSSProperties, ReactNode } from \"react\";\nimport \"./LiquidChrome.css\";\n\nexport interface LiquidChromeProps {\n  /** Contenido que va encima del metal. */\n  children?: ReactNode;\n  /** Qué tan rápido fluye el metal. */\n  speed?: number;\n  /** Tamaño de las olas (menor = más olas). */\n  scale?: number;\n  /** Qué tanto ondula al paso del cursor. */\n  reactivity?: number;\n  /** Brillo del entorno reflejado. */\n  intensity?: number;\n  /** Color del metal (plata azulada por defecto). */\n  tint?: string;\n  /** Clase extra para el contenedor. */\n  className?: string;\n  /** Estilos extra para el contenedor. */\n  style?: CSSProperties;\n}\n\nconst MAX_GOTAS = 12;\n\nconst VERT = `#version 300 es\nvoid main() {\n  vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));\n  gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);\n}`;\n\nconst FRAG = `#version 300 es\nprecision highp float;\n#define MAX_GOTAS ${MAX_GOTAS}\nuniform vec2 u_res;\nuniform float u_t;          // tiempo de flujo del metal\nuniform float u_now;        // reloj para la edad de las gotas\nuniform float u_scale;\nuniform float u_int;\nuniform vec3 u_tint;\nuniform vec2 u_ptr;         // cursor suavizado, en el espacio de la superficie\nuniform float u_dent;       // cuánto empuja el cursor la superficie\nuniform vec4 u_gotas[MAX_GOTAS]; // x, y, nacimiento, amplitud\nout vec4 o;\n\nvec3 rotY(vec3 v, float a) { float c = cos(a), s = sin(a); return vec3(c * v.x + s * v.z, v.y, -s * v.x + c * v.z); }\nvec3 rotX(vec3 v, float a) { float c = cos(a), s = sin(a); return vec3(v.x, c * v.y - s * v.z, s * v.y + c * v.z); }\nfloat wrapd(float a) { return a - 6.2831853 * floor((a + 3.1415926) / 6.2831853); }\nfloat rbox(float az, float el, float caz, float cel, float hw, float hh, float soft) {\n  float a = abs(wrapd(az - caz)), e = abs(el - cel);\n  return smoothstep(hw + soft, hw - soft, a) * smoothstep(hh + soft, hh - soft, e);\n}\nfloat hash(vec2 p) { return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453); }\n\n// Estudio: cielo azul profundo arriba, horizonte blanco, suelo oscuro y cajas de luz de varios tamaños.\nvec3 studio(vec3 d) {\n  float el = asin(clamp(d.y, -1.0, 1.0));\n  float az = atan(d.x, d.z);\n  float up = clamp(el / 1.25, 0.0, 1.0);\n  vec3 sky = mix(vec3(0.92, 0.96, 1.00), vec3(0.14, 0.22, 0.42), pow(up, 0.6));\n  float dn = clamp(-el / 1.1, 0.0, 1.0);\n  vec3 gnd = mix(vec3(0.50, 0.49, 0.50), vec3(0.02, 0.022, 0.03), pow(dn, 0.55));\n  vec3 c = el >= 0.0 ? sky : gnd;\n  c += vec3(1.0, 0.98, 0.95) * 2.6 * exp(-pow(el / 0.03, 2.0));\n  c += vec3(1.00, 0.96, 0.90) * 8.0 * rbox(az, el, -0.85, 0.50, 0.30, 0.20, 0.04);\n  c += vec3(0.86, 0.93, 1.00) * 7.0 * rbox(az, el, 1.00, 0.28, 0.06, 0.46, 0.035);\n  c += vec3(1.00) * 5.0 * rbox(az, el, 0.10, 1.05, 0.55, 0.08, 0.04);\n  c += vec3(1.00, 0.88, 0.76) * 5.0 * rbox(az, el, 0.45, -0.34, 0.40, 0.045, 0.03);\n  c += vec3(0.80, 0.90, 1.00) * 4.0 * rbox(az, el, -1.55, -0.2, 0.05, 0.30, 0.03);\n  c += vec3(1.00) * 4.0 * rbox(az, el, 1.65, 0.80, 0.16, 0.10, 0.03);\n  return c;\n}\n\n// Campo de alturas: metal que fluye (dominio deformado) + abolladura bajo el cursor + ondas de las gotas.\nfloat H(vec2 p) {\n  float t = u_t;\n  vec2 q = p;\n  q += 0.50 * vec2(sin(p.y * 1.25 + t * 0.55), cos(p.x * 1.05 - t * 0.47));\n  q += 0.28 * vec2(sin(q.y * 2.30 - t * 0.71 + 1.7), cos(q.x * 2.00 + t * 0.62));\n  float h = 0.55 * sin(q.x * 1.65 + q.y * 1.05 + t * 0.35);\n  h += 0.28 * sin(q.x * 3.05 - q.y * 2.35 - t * 0.48);\n  h += 0.30 * sin(length(q - vec2(0.3, -0.2)) * 3.3 - t * 0.55);\n  // Abolladura que sigue al cursor.\n  vec2 d = p - u_ptr;\n  h -= u_dent * 0.45 * exp(-dot(d, d) * 4.0);\n  // Ondas circulares: cada gota viaja hacia afuera y se apaga.\n  for (int i = 0; i < MAX_GOTAS; i++) {\n    vec4 g = u_gotas[i];\n    if (g.w <= 0.0) continue;\n    float age = u_now - g.z;\n    float r = length(p - g.xy);\n    float x = r - age * 1.05;\n    h += g.w * 0.085 * cos(x * 9.5) * exp(-x * x * 5.0) * exp(-age * 1.1) / (1.0 + r * 1.5);\n  }\n  return h;\n}\n\nvoid main() {\n  vec2 p = (gl_FragCoord.xy - 0.5 * u_res) / u_res.y * 3.4 * u_scale;\n  float e = 2.0 / u_res.y * 3.4 * u_scale;\n  float h0 = H(p);\n  float hx = H(p + vec2(e, 0.0));\n  float hy = H(p + vec2(0.0, e));\n  vec2 gr = vec2(hx - h0, hy - h0) / e;\n  vec3 N = normalize(vec3(-gr * 0.23, 1.0));\n  vec3 V = normalize(vec3((gl_FragCoord.xy - 0.5 * u_res) / u_res.y * 0.45, 1.0));\n  float ndv = max(dot(N, V), 0.0);\n  vec3 R = reflect(-V, N);\n  float yaw = u_t * 0.045 + u_ptr.x * 0.10;\n  float pitch = 0.10 + u_ptr.y * 0.06;\n  float dsp = 0.03 * (1.0 - ndv) + 0.006;\n  vec3 rf;\n  rf.r = studio(rotX(rotY(R, yaw - dsp), pitch)).r;\n  rf.g = studio(rotX(rotY(R, yaw), pitch)).g;\n  rf.b = studio(rotX(rotY(R, yaw + dsp), pitch)).b;\n  vec3 F0 = u_tint * 0.93;\n  vec3 F = F0 + (1.0 - F0) * pow(1.0 - ndv, 5.0);\n  vec3 col = rf * F * u_int;\n  col = 1.0 - exp(-col * 1.5);\n  col = pow(col, vec3(0.9));\n  vec2 vv = gl_FragCoord.xy / u_res - 0.5;\n  col *= 1.0 - 0.45 * dot(vv, vv);\n  col += (hash(gl_FragCoord.xy) - 0.5) / 255.0;\n  o = vec4(col, 1.0);\n}`;\n\nfunction colorARgb(css: string): [number, number, number] {\n  try {\n    const c = document.createElement(\"canvas\");\n    c.width = c.height = 1;\n    const x = c.getContext(\"2d\", { willReadFrequently: true })!;\n    x.fillStyle = \"#e3eaf5\";\n    x.fillStyle = css;\n    x.fillRect(0, 0, 1, 1);\n    const d = x.getImageData(0, 0, 1, 1).data;\n    return [d[0] / 255, d[1] / 255, d[2] / 255];\n  } catch {\n    return [0.89, 0.92, 0.96];\n  }\n}\n\nfunction compilar(gl: WebGL2RenderingContext, vs: string, fs: string): WebGLProgram | null {\n  const mk = (tipo: number, src: string) => {\n    const s = gl.createShader(tipo)!;\n    gl.shaderSource(s, src);\n    gl.compileShader(s);\n    if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) { console.error(gl.getShaderInfoLog(s)); return null; }\n    return s;\n  };\n  const a = mk(gl.VERTEX_SHADER, vs), b = mk(gl.FRAGMENT_SHADER, fs);\n  if (!a || !b) return null;\n  const p = gl.createProgram()!;\n  gl.attachShader(p, a); gl.attachShader(p, b);\n  gl.linkProgram(p);\n  gl.deleteShader(a); gl.deleteShader(b);\n  if (!gl.getProgramParameter(p, gl.LINK_STATUS)) { console.error(gl.getProgramInfoLog(p)); return null; }\n  return p;\n}\n\n/** Fondo de cromo líquido: una lámina de metal que fluye, refleja un estudio y ondula cuando pasas el cursor. */\nexport function LiquidChrome({\n  children,\n  speed = 0.6,\n  scale = 1,\n  reactivity = 1,\n  intensity = 1,\n  tint = \"#e3eaf5\",\n  className = \"\",\n  style,\n}: LiquidChromeProps) {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const props = useRef({ speed, scale, reactivity, intensity, tint });\n  props.current = { speed, scale, reactivity, intensity, tint };\n  const wakeRef = useRef<(() => void) | null>(null);\n\n  useEffect(() => {\n    const root = rootRef.current;\n    if (!root) return;\n    const canvas = document.createElement(\"canvas\");\n    canvas.className = \"furin-liquid-chrome__lienzo\";\n    canvas.setAttribute(\"aria-hidden\", \"true\");\n    const gl = canvas.getContext(\"webgl2\", { alpha: false, antialias: false, powerPreference: \"high-performance\" });\n    if (!gl) return; // sin WebGL2 queda el cromo de CSS\n    root.insertBefore(canvas, root.firstChild);\n\n    const reducido = matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    let prog: WebGLProgram | null = null;\n    let U: Record<string, WebGLUniformLocation | null> = {};\n    let visible = true;\n    let raf = 0;\n    let muerto = false;\n    let w = 0, h = 0;\n\n    const iniciarGl = () => {\n      prog = compilar(gl, VERT, FRAG);\n      if (!prog) return;\n      for (const k of [\"u_res\", \"u_t\", \"u_now\", \"u_scale\", \"u_int\", \"u_tint\", \"u_ptr\", \"u_dent\", \"u_gotas\"]) U[k] = gl.getUniformLocation(prog, k);\n      gl.useProgram(prog);\n    };\n\n    // Estado: flujo del metal, cursor suavizado y gotas (ondas) que nacen al mover el mouse.\n    const gotas = new Float32Array(MAX_GOTAS * 4);\n    let sigGota = 0;\n    const est = { t: reducido ? 7.3 : 2.0, ult: 0, px: 0, py: 0, tx: 0, ty: 0, dent: 0, dentObj: 0, dentirDentro: false, lx: 0, ly: 0, lt: 0, hayPtr: false };\n    const ahora = () => performance.now() / 1000;\n    let t0 = ahora();\n\n    const pintar = (now: number) => {\n      if (!prog || gl.isContextLost() || !w) return;\n      const p = props.current;\n      gl.viewport(0, 0, canvas.width, canvas.height);\n      gl.useProgram(prog);\n      gl.uniform2f(U.u_res, canvas.width, canvas.height);\n      gl.uniform1f(U.u_t, est.t);\n      gl.uniform1f(U.u_now, now);\n      gl.uniform1f(U.u_scale, p.scale);\n      gl.uniform1f(U.u_int, p.intensity);\n      const [r, g, b] = colorARgb(p.tint);\n      gl.uniform3f(U.u_tint, r, g, b);\n      gl.uniform2f(U.u_ptr, est.px, est.py);\n      gl.uniform1f(U.u_dent, est.dent * p.reactivity);\n      gl.uniform4fv(U.u_gotas, gotas);\n      gl.drawArrays(gl.TRIANGLES, 0, 3);\n    };\n\n    const hayGotas = (now: number) => {\n      for (let i = 0; i < MAX_GOTAS; i++) if (gotas[i * 4 + 3] > 0 && now - gotas[i * 4 + 2] < 4.5) return true;\n      return false;\n    };\n\n    const paso = () => {\n      raf = 0;\n      if (muerto || !visible) return;\n      const now = ahora() - t0;\n      const dt = Math.min(0.05, now - est.ult || 0.016);\n      est.ult = now;\n      if (!reducido) est.t += dt * props.current.speed;\n      // El cursor se suaviza con un resorte crítico; la abolladura se llena y se vacía con inercia.\n      const k = 1 - Math.exp(-dt * 9);\n      est.px += (est.tx - est.px) * k;\n      est.py += (est.ty - est.py) * k;\n      est.dent += (est.dentObj - est.dent) * (1 - Math.exp(-dt * 4));\n      for (let i = 0; i < MAX_GOTAS; i++) if (gotas[i * 4 + 3] > 0 && now - gotas[i * 4 + 2] > 4.5) gotas[i * 4 + 3] = 0;\n      pintar(now);\n      // Con speed 0 y sin cursor ni gotas el metal está quieto: el bucle duerme.\n      if ((!reducido && props.current.speed > 0) || hayGotas(now) || Math.abs(est.dent - est.dentObj) > 0.01 || Math.hypot(est.tx - est.px, est.ty - est.py) > 0.002) raf = requestAnimationFrame(paso);\n    };\n    const wake = () => { if (!raf && visible && !muerto) { est.ult = ahora() - t0; raf = requestAnimationFrame(paso); } };\n    wakeRef.current = wake;\n\n    // ── Cursor ──\n    const aSuperficie = (e: PointerEvent) => {\n      const r = root.getBoundingClientRect();\n      const s = 3.4 * props.current.scale / Math.max(r.height, 1);\n      return [(e.clientX - r.left - r.width / 2) * s, -(e.clientY - r.top - r.height / 2) * s] as const;\n    };\n    const onMove = (e: PointerEvent) => {\n      if (reducido) return;\n      const [x, y] = aSuperficie(e);\n      if (!est.hayPtr) { est.px = x; est.py = y; est.hayPtr = true; }\n      est.tx = x; est.ty = y;\n      est.dentObj = 1;\n      const now = ahora() - t0;\n      const dist = Math.hypot(x - est.lx, y - est.ly);\n      if (dist > 0.38 || !est.lt) {\n        const vel = Math.min(1.6, dist / Math.max(now - est.lt, 0.016) * 0.35);\n        const i = sigGota++ % MAX_GOTAS;\n        gotas[i * 4] = x; gotas[i * 4 + 1] = y; gotas[i * 4 + 2] = now; gotas[i * 4 + 3] = 0.3 + Math.min(vel, 0.9);\n        est.lx = x; est.ly = y; est.lt = now;\n      }\n      wake();\n    };\n    const onLeave = () => { est.dentObj = 0; est.hayPtr = false; est.lt = 0; wake(); };\n    if (!reducido) {\n      root.addEventListener(\"pointermove\", onMove, { passive: true });\n      root.addEventListener(\"pointerdown\", onMove, { passive: true });\n      root.addEventListener(\"pointerleave\", onLeave);\n    }\n\n    // ── Tamaño ──\n    const medir = () => {\n      const dpr = Math.min(window.devicePixelRatio || 1, 1.5);\n      const r = root.getBoundingClientRect();\n      w = Math.max(1, Math.round(r.width * dpr)); h = Math.max(1, Math.round(r.height * dpr));\n      if (canvas.width !== w || canvas.height !== h) { canvas.width = w; canvas.height = h; }\n      pintar(ahora() - t0);\n      wake();\n    };\n    const ro = new ResizeObserver(medir);\n    ro.observe(root);\n    const io = new IntersectionObserver(([e]) => { visible = e.isIntersecting; if (visible) wake(); }, { rootMargin: \"80px\" });\n    io.observe(root);\n\n    canvas.addEventListener(\"webglcontextlost\", (e) => { e.preventDefault(); cancelAnimationFrame(raf); raf = 0; });\n    canvas.addEventListener(\"webglcontextrestored\", () => { U = {}; iniciarGl(); medir(); });\n\n    iniciarGl();\n    medir();\n\n    return () => {\n      muerto = true;\n      wakeRef.current = null;\n      cancelAnimationFrame(raf);\n      io.disconnect(); ro.disconnect();\n      root.removeEventListener(\"pointermove\", onMove);\n      root.removeEventListener(\"pointerdown\", onMove);\n      root.removeEventListener(\"pointerleave\", onLeave);\n      gl.getExtension(\"WEBGL_lose_context\")?.loseContext();\n      canvas.remove();\n    };\n  }, []);\n\n  // Si cambian las props mientras el bucle dormía (speed 0), hay que repintar.\n  useEffect(() => { wakeRef.current?.(); }, [speed, scale, reactivity, intensity, tint]);\n\n  return (\n    <div ref={rootRef} className={`furin-liquid-chrome ${className}`} style={style}>\n      {children != null ? <div className=\"furin-liquid-chrome__contenido\">{children}</div> : null}\n    </div>\n  );\n}\n\nexport default LiquidChrome;\n",
      "type": "registry:component",
      "target": "components/furin/liquid-chrome/LiquidChrome.tsx"
    },
    {
      "path": "registry/liquid-chrome/LiquidChrome.css",
      "content": ".furin-liquid-chrome {\n  position: relative;\n  display: grid;\n  place-items: center;\n  width: 100%;\n  height: 100%;\n  min-height: 160px;\n  align-self: stretch;\n  justify-self: stretch;\n  overflow: hidden;\n  isolation: isolate;\n  /* Mientras WebGL no llega (o no existe) queda un cromo de CSS: cielo, horizonte y suelo. */\n  background:\n    radial-gradient(120% 70% at 30% 18%, rgba(255, 255, 255, 0.85), transparent 55%),\n    linear-gradient(172deg, #dfe8f5 0%, #8d9bb3 34%, #1a1d26 49%, #05060a 50%, #6f7a8f 72%, #cfd8e6 100%);\n  touch-action: pan-y;\n}\n.furin-liquid-chrome__lienzo {\n  position: absolute;\n  inset: 0;\n  width: 100%;\n  height: 100%;\n  display: block;\n  z-index: 0;\n}\n.furin-liquid-chrome__contenido {\n  position: relative;\n  z-index: 1;\n  max-width: 100%;\n}\n",
      "type": "registry:file",
      "target": "components/furin/liquid-chrome/LiquidChrome.css"
    }
  ]
}
