{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chrome-text",
  "type": "registry:component",
  "title": "Chrome Text",
  "description": "Liquid chrome letters whose reflection follows the cursor.",
  "author": "Marcelo Treviño Velázquez <https://furin.dev>",
  "dependencies": [],
  "categories": [
    "Text"
  ],
  "docs": "Installed in components/furin/chrome-text/. Docs, live controls and props table: https://furin.dev/components/chrome-text/",
  "files": [
    {
      "path": "registry/chrome-text/ChromeText.tsx",
      "content": "\"use client\";\nimport { useEffect, useRef } from \"react\";\nimport type { CSSProperties } from \"react\";\nimport \"./ChromeText.css\";\n\nexport interface ChromeTextProps {\n  /** El texto que se vuelve cromo (acepta saltos de línea). */\n  children?: string;\n  /** Tamaño de la letra, en px. */\n  size?: number;\n  /** Qué tan infladas y redondeadas se ven las letras (0 a 1). */\n  bevel?: number;\n  /** Engrosa las letras como si fueran más pesadas (0 a 1). */\n  thickness?: number;\n  /** Brillo del entorno que refleja el metal. */\n  intensity?: number;\n  /** Color del metal (plata azulada por defecto). */\n  tint?: string;\n  /** Deja que el reflejo pasee solo cuando no hay cursor. */\n  sweep?: boolean;\n  /** Clase extra para el contenedor. */\n  className?: string;\n  /** Estilos extra para el contenedor. */\n  style?: CSSProperties;\n}\n\n/* ───────────── Shaders ───────────── */\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\n// Campo de pendientes (rg), sombra (b) y máscara (a) vienen de la CPU; aquí solo se sombrea.\nconst FRAG = `#version 300 es\nprecision highp float;\nuniform sampler2D u_campo;\nuniform vec2 u_res;\nuniform vec2 u_ptr;\nuniform vec3 u_tint;\nuniform float u_int;\nuniform float u_bevel;\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}\n\n// Estudio procedural: cielo azulado, horizonte brillante, suelo oscuro y cuatro cajas de luz.\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.2, 0.0, 1.0);\n  vec3 sky = mix(vec3(0.86, 0.92, 1.00), vec3(0.07, 0.12, 0.26), pow(up, 0.55));\n  float dn = clamp(-el / 1.1, 0.0, 1.0);\n  vec3 gnd = mix(vec3(0.30, 0.29, 0.29), vec3(0.010, 0.011, 0.016), pow(dn, 0.45));\n  vec3 c = el >= 0.0 ? sky : gnd;\n  c += vec3(1.0, 0.98, 0.94) * 2.2 * exp(-pow(el / 0.022, 2.0));\n  c += vec3(1.00, 0.96, 0.90) * 7.0 * rbox(az, el, -0.80, 0.52, 0.32, 0.20, 0.05);\n  c += vec3(0.86, 0.93, 1.00) * 6.0 * rbox(az, el, 1.05, 0.30, 0.07, 0.44, 0.04);\n  c += vec3(1.00) * 4.0 * rbox(az, el, 0.20, 1.10, 0.60, 0.09, 0.05);\n  c += vec3(1.00, 0.90, 0.80) * 3.0 * rbox(az, el, 2.60, 0.10, 0.10, 0.50, 0.06);\n  c += vec3(0.80, 0.88, 1.00) * 2.0 * rbox(az, el, -1.90, -0.38, 0.50, 0.07, 0.05);\n  return c;\n}\n\nfloat hash(vec2 p) { return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453); }\n\nvoid main() {\n  vec2 px = vec2(gl_FragCoord.x, u_res.y - gl_FragCoord.y);\n  vec2 uv = px / u_res;\n  vec4 f = texture(u_campo, uv);\n  float a = f.a;\n  float sh = f.b;\n  vec3 col = vec3(0.0);\n  if (a > 0.002) {\n    vec2 g = f.rg * u_bevel;\n    // Un poco de curvatura global: así la cara plana también degrada de cielo a suelo.\n    vec2 cv = vec2((uv.x - 0.5) * 0.30, (0.5 - uv.y) * 0.62);\n    vec3 N = normalize(vec3(-g.x + cv.x, g.y + cv.y, 1.0));\n    vec3 V = vec3(0.0, 0.0, 1.0);\n    float ndv = max(dot(N, V), 0.0);\n    vec3 R = reflect(-V, N);\n    float yaw = u_ptr.x * 0.95;\n    float pitch = u_ptr.y * 0.30;\n    float dsp = 0.045 * (1.0 - ndv);\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.92;\n    vec3 F = F0 + (1.0 - F0) * pow(1.0 - ndv, 5.0);\n    col = rf * F * u_int;\n    // Destello puntual que acompaña al cursor.\n    vec3 L = normalize(vec3(u_ptr.x * 0.85, u_ptr.y * 0.85 + 0.1, 0.55));\n    float nh = max(dot(N, normalize(L + V)), 0.0);\n    col += vec3(1.0, 0.98, 0.95) * (pow(nh, 320.0) * 2.4 + pow(nh, 36.0) * 0.12);\n    col = 1.0 - exp(-col * 1.7);\n    col = pow(col, vec3(0.92));\n    col += (hash(gl_FragCoord.xy) - 0.5) / 255.0;\n  }\n  float oa = a + sh * (1.0 - a);\n  o = vec4(col * a, oa);\n}`;\n\n/* ───────────── Campo de alturas del texto (CPU) ───────────── */\n\nconst INF = 1e20;\n\n/** Transformada de distancia euclídea (Felzenszwalb): distancia² de cada píxel de \"dentro\" al \"fuera\" más cercano. */\nfunction edt(inside: Uint8Array, w: number, h: number): Float32Array {\n  const g = new Float32Array(w * h);\n  for (let i = 0; i < g.length; i++) g[i] = inside[i] ? INF : 0;\n  const n = Math.max(w, h);\n  const f = new Float32Array(n), d = new Float32Array(n), z = new Float32Array(n + 1), v = new Int32Array(n);\n  const pasada = (len: number) => {\n    let k = 0;\n    v[0] = 0; z[0] = -INF; z[1] = INF;\n    for (let q = 1; q < len; q++) {\n      let s = (f[q] + q * q - (f[v[k]] + v[k] * v[k])) / (2 * q - 2 * v[k]);\n      while (s <= z[k]) { k--; s = (f[q] + q * q - (f[v[k]] + v[k] * v[k])) / (2 * q - 2 * v[k]); }\n      k++; v[k] = q; z[k] = s; z[k + 1] = INF;\n    }\n    k = 0;\n    for (let q = 0; q < len; q++) {\n      while (z[k + 1] < q) k++;\n      d[q] = (q - v[k]) * (q - v[k]) + f[v[k]];\n    }\n  };\n  for (let x = 0; x < w; x++) {\n    for (let y = 0; y < h; y++) f[y] = g[y * w + x];\n    pasada(h);\n    for (let y = 0; y < h; y++) g[y * w + x] = d[y];\n  }\n  for (let y = 0; y < h; y++) {\n    for (let x = 0; x < w; x++) f[x] = g[y * w + x];\n    pasada(w);\n    for (let x = 0; x < w; x++) g[y * w + x] = d[x];\n  }\n  return g;\n}\n\n/** Desenfoque de caja separable (3 pasadas ≈ gaussiana). Modifica y devuelve `a`. */\nfunction blur(a: Float32Array, w: number, h: number, r: number, pasadas = 3): Float32Array {\n  r = Math.max(1, Math.round(r));\n  const t = new Float32Array(Math.max(w, h));\n  const inv = 1 / (2 * r + 1);\n  for (let p = 0; p < pasadas; p++) {\n    for (let y = 0; y < h; y++) {\n      const o = y * w;\n      let s = 0;\n      for (let i = -r; i <= r; i++) s += a[o + Math.min(w - 1, Math.max(0, i))];\n      for (let x = 0; x < w; x++) {\n        t[x] = s * inv;\n        s += a[o + Math.min(w - 1, x + r + 1)] - a[o + Math.max(0, x - r)];\n      }\n      for (let x = 0; x < w; x++) a[o + x] = t[x];\n    }\n    for (let x = 0; x < w; x++) {\n      let s = 0;\n      for (let i = -r; i <= r; i++) s += a[Math.min(h - 1, Math.max(0, i)) * w + x];\n      for (let y = 0; y < h; y++) {\n        t[y] = s * inv;\n        s += a[Math.min(h - 1, y + r + 1) * w + x] - a[Math.max(0, y - r) * w + x];\n      }\n      for (let y = 0; y < h; y++) a[y * w + x] = t[y];\n    }\n  }\n  return a;\n}\n\ninterface Campo { w: number; h: number; datos: Float32Array }\n\n/** Dibuja el texto en un lienzo 2D alineado con el DOM y lo convierte en pendientes, sombra y máscara. */\nfunction construirCampo(textEl: HTMLElement, texto: string, pad: number, dpr: number, bevel: number, thickness: number): Campo {\n  const cs = getComputedStyle(textEl);\n  const fs = parseFloat(cs.fontSize) || 96;\n  const cw = textEl.offsetWidth, ch = textEl.offsetHeight;\n  const W = Math.max(8, Math.ceil((cw + pad * 2) * dpr)), H = Math.max(8, Math.ceil((ch + pad * 2) * dpr));\n  const cv = document.createElement(\"canvas\");\n  cv.width = W; cv.height = H;\n  const ctx = cv.getContext(\"2d\", { willReadFrequently: true })!;\n  ctx.font = `${cs.fontStyle} ${cs.fontWeight} ${fs * dpr}px ${cs.fontFamily}`;\n  const ls = parseFloat(cs.letterSpacing);\n  if (\"letterSpacing\" in ctx) (ctx as unknown as { letterSpacing: string }).letterSpacing = `${(Number.isFinite(ls) ? ls : 0) * dpr}px`;\n  ctx.textBaseline = \"alphabetic\";\n  ctx.fillStyle = \"#fff\";\n  ctx.strokeStyle = \"#fff\";\n  ctx.lineJoin = \"round\";\n  ctx.lineWidth = fs * dpr * 0.1 * thickness;\n  const m = ctx.measureText(\"Hg\");\n  const asc = m.fontBoundingBoxAscent ?? fs * 0.95, desc = m.fontBoundingBoxDescent ?? fs * 0.25;\n  const lineas = texto.split(\"\\n\");\n  const lh = ch / lineas.length * dpr;\n  const al = cs.textAlign;\n  lineas.forEach((ln, i) => {\n    const wl = ctx.measureText(ln).width;\n    const x = pad * dpr + (al === \"left\" || al === \"start\" ? 0 : al === \"right\" || al === \"end\" ? cw * dpr - wl : (cw * dpr - wl) / 2);\n    const base = pad * dpr + i * lh + (lh - (asc + desc)) / 2 + asc;\n    ctx.fillText(ln, x, base);\n    // Un contorno del mismo color engorda la letra sin depender del peso de la tipografía.\n    if (thickness > 0) ctx.strokeText(ln, x, base);\n  });\n  const img = ctx.getImageData(0, 0, W, H).data;\n  const n = W * H;\n  const alfa = new Float32Array(n);\n  const dentro = new Uint8Array(n);\n  for (let i = 0; i < n; i++) { alfa[i] = img[i * 4 + 3] / 255; dentro[i] = alfa[i] > 0.5 ? 1 : 0; }\n\n  // Altura: perfil de cuarto de círculo medido desde el borde, así las letras salen \"infladas\".\n  const R = Math.max(2, fs * dpr * (0.04 + 0.2 * bevel));\n  const d2 = edt(dentro, W, H);\n  const hgt = new Float32Array(n);\n  for (let i = 0; i < n; i++) {\n    if (!dentro[i]) continue;\n    const t = Math.min(1, Math.max(0, Math.sqrt(d2[i]) - 0.5) / R);\n    hgt[i] = R * Math.sqrt(1 - (1 - t) * (1 - t));\n  }\n  blur(hgt, W, H, Math.max(1.5, R * 0.24), 3);\n\n  // Sombra suave, desplazada hacia abajo.\n  const sombra = blur(Float32Array.from(alfa), W, H, fs * dpr * 0.035, 3);\n  const off = Math.round(fs * dpr * 0.045);\n\n  const datos = new Float32Array(n * 4);\n  for (let y = 0; y < H; y++) {\n    for (let x = 0; x < W; x++) {\n      const i = y * W + x;\n      const xl = hgt[y * W + Math.max(0, x - 1)], xr = hgt[y * W + Math.min(W - 1, x + 1)];\n      const yu = hgt[Math.max(0, y - 1) * W + x], yd = hgt[Math.min(H - 1, y + 1) * W + x];\n      const o = i * 4;\n      datos[o] = Math.max(-6, Math.min(6, (xr - xl) * 0.5));\n      datos[o + 1] = Math.max(-6, Math.min(6, (yd - yu) * 0.5));\n      datos[o + 2] = sombra[Math.max(0, y - off) * W + x] * 0.62;\n      datos[o + 3] = alfa[i];\n    }\n  }\n  return { w: W, h: H, datos };\n}\n\n/* ───────────── Utilidades ───────────── */\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 = \"#e4e9f0\";\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.91, 0.94];\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\ninterface Vivo { rebuild: () => void; wake: () => void }\n\n/** Texto de cromo líquido: letras infladas de metal que reflejan un estudio y siguen al cursor. */\nexport function ChromeText({\n  children = \"Chrome\",\n  size = 96,\n  bevel = 0.5,\n  thickness = 0.5,\n  intensity = 1,\n  tint = \"#e4e9f0\",\n  sweep = true,\n  className = \"\",\n  style,\n}: ChromeTextProps) {\n  const rootRef = useRef<HTMLSpanElement>(null);\n  const textRef = useRef<HTMLSpanElement>(null);\n  const vivo = useRef<Vivo | null>(null);\n  const texto = String(children ?? \"\");\n  const props = useRef({ texto, size, bevel, thickness, intensity, tint, sweep });\n  props.current = { texto, size, bevel, thickness, intensity, tint, sweep };\n\n  useEffect(() => {\n    const root = rootRef.current, textEl = textRef.current;\n    if (!root || !textEl) return;\n    const canvas = document.createElement(\"canvas\");\n    canvas.className = \"furin-chrome-text__lienzo\";\n    canvas.setAttribute(\"aria-hidden\", \"true\");\n    const gl = canvas.getContext(\"webgl2\", { alpha: true, premultipliedAlpha: true, antialias: false });\n    if (!gl) return; // sin WebGL2 queda el degradado de CSS\n    root.appendChild(canvas);\n\n    const reducido = matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    let prog: WebGLProgram | null = null;\n    let tex: WebGLTexture | null = null;\n    let U: Record<string, WebGLUniformLocation | null> = {};\n    let listo = false;\n    let visible = true;\n    let raf = 0;\n    let muerto = false;\n\n    const iniciarGl = () => {\n      prog = compilar(gl, VERT, FRAG);\n      if (!prog) return;\n      for (const k of [\"u_campo\", \"u_res\", \"u_ptr\", \"u_tint\", \"u_int\", \"u_bevel\"]) U[k] = gl.getUniformLocation(prog, k);\n      tex = gl.createTexture();\n      gl.useProgram(prog);\n      gl.uniform1i(U.u_campo, 0);\n      gl.disable(gl.DEPTH_TEST);\n      gl.disable(gl.BLEND);\n    };\n\n    // Estado del reflejo: un resorte lleva (x, y) hacia el cursor, o hacia un paseo lento si no hay cursor.\n    const est = { x: 0.35, y: -0.05, vx: 0, vy: 0, tx: 0.35, ty: -0.05, auto: true, t0: performance.now(), ultimo: 0 };\n\n    const pintar = () => {\n      if (!prog || !listo || gl.isContextLost()) 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.uniform2f(U.u_ptr, est.x, est.y);\n      const [r, g, b] = colorARgb(p.tint);\n      gl.uniform3f(U.u_tint, r, g, b);\n      gl.uniform1f(U.u_int, p.intensity);\n      gl.uniform1f(U.u_bevel, 0.85 + p.bevel * 0.6);\n      gl.clearColor(0, 0, 0, 0);\n      gl.clear(gl.COLOR_BUFFER_BIT);\n      gl.drawArrays(gl.TRIANGLES, 0, 3);\n    };\n\n    const rebuild = () => {\n      if (muerto || gl.isContextLost() || !prog) return;\n      const p = props.current;\n      const dpr = Math.min(window.devicePixelRatio || 1, 2);\n      const pad = Math.round(p.size * 0.3);\n      const cw = textEl.offsetWidth, ch = textEl.offsetHeight;\n      if (!cw || !ch) return;\n      const campo = construirCampo(textEl, p.texto, pad, dpr, p.bevel, p.thickness);\n      canvas.width = campo.w; canvas.height = campo.h;\n      canvas.style.width = `${cw + pad * 2}px`;\n      canvas.style.height = `${ch + pad * 2}px`;\n      canvas.style.left = `${-pad}px`;\n      canvas.style.top = `${-pad}px`;\n      gl.activeTexture(gl.TEXTURE0);\n      gl.bindTexture(gl.TEXTURE_2D, tex);\n      gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);\n      gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, campo.w, campo.h, 0, gl.RGBA, gl.FLOAT, campo.datos);\n      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);\n      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n      listo = true;\n      pintar();\n      root.setAttribute(\"data-gl\", \"1\");\n      wake();\n    };\n\n    // ── Bucle: duerme cuando está fuera de pantalla o el resorte ya se asentó ──\n    const quieto = () => !p_auto() && Math.abs(est.vx) + Math.abs(est.vy) < 1e-3 && Math.abs(est.tx - est.x) + Math.abs(est.ty - est.y) < 1e-3;\n    const p_auto = () => est.auto && props.current.sweep && !reducido;\n    const paso = (now: number) => {\n      raf = 0;\n      if (muerto || !visible || !listo) return;\n      const dt = Math.min(0.05, (now - (est.ultimo || now)) / 1000 || 0.016);\n      est.ultimo = now;\n      if (p_auto()) {\n        const t = (now - est.t0) / 1000;\n        est.tx = Math.sin(t * 0.55) * 0.9;\n        est.ty = Math.cos(t * 0.37) * 0.32;\n      }\n      // Resorte ligeramente subamortiguado: llega con un pequeño rebote.\n      const k = 120, c = 15;\n      est.vx += ((est.tx - est.x) * k - est.vx * c) * dt;\n      est.vy += ((est.ty - est.y) * k - est.vy * c) * dt;\n      est.x += est.vx * dt;\n      est.y += est.vy * dt;\n      pintar();\n      if (!quieto()) raf = requestAnimationFrame(paso);\n      else est.ultimo = 0;\n    };\n    const wake = () => {\n      if (!raf && visible && listo && !muerto && !reducido) { est.ultimo = 0; raf = requestAnimationFrame(paso); }\n    };\n\n    // ── Cursor: se mide contra el propio texto, aunque el mouse ande cerca ──\n    const onMove = (e: PointerEvent) => {\n      const r = canvas.getBoundingClientRect();\n      if (!r.width) return;\n      const cx = r.left + r.width / 2, cy = r.top + r.height / 2;\n      est.tx = Math.max(-1.4, Math.min(1.4, (e.clientX - cx) / Math.max(r.width * 0.5, 160)));\n      est.ty = Math.max(-1.0, Math.min(1.0, -(e.clientY - cy) / Math.max(r.height * 0.5, 120)));\n      est.auto = false;\n      wake();\n    };\n    const onLeave = () => { est.auto = true; est.t0 = performance.now() - (Math.asin(Math.max(-1, Math.min(1, est.x / 0.9))) / 0.55) * 1000; wake(); };\n    const onUp = (e: PointerEvent) => { if (e.pointerType !== \"mouse\") onLeave(); };\n    if (!reducido) {\n      window.addEventListener(\"pointermove\", onMove, { passive: true });\n      window.addEventListener(\"pointerdown\", onMove, { passive: true });\n      document.documentElement.addEventListener(\"pointerleave\", onLeave);\n      window.addEventListener(\"pointerup\", onUp, { passive: true });\n    } else {\n      est.x = est.tx = 0.35; est.y = est.ty = -0.05;\n    }\n\n    const io = new IntersectionObserver(([e]) => { visible = e.isIntersecting; if (visible) wake(); }, { rootMargin: \"80px\" });\n    io.observe(root);\n    let pend = 0;\n    const pedirRebuild = () => { cancelAnimationFrame(pend); pend = requestAnimationFrame(rebuild); };\n    const ro = new ResizeObserver(pedirRebuild);\n    ro.observe(textEl);\n    document.fonts?.ready.then(pedirRebuild);\n    document.fonts?.addEventListener?.(\"loadingdone\", pedirRebuild);\n\n    canvas.addEventListener(\"webglcontextlost\", (e) => { e.preventDefault(); listo = false; cancelAnimationFrame(raf); raf = 0; });\n    canvas.addEventListener(\"webglcontextrestored\", () => { U = {}; iniciarGl(); rebuild(); });\n\n    iniciarGl();\n    vivo.current = { rebuild, wake };\n    rebuild();\n\n    return () => {\n      muerto = true;\n      vivo.current = null;\n      cancelAnimationFrame(raf);\n      cancelAnimationFrame(pend);\n      io.disconnect(); ro.disconnect();\n      document.fonts?.removeEventListener?.(\"loadingdone\", pedirRebuild);\n      window.removeEventListener(\"pointermove\", onMove);\n      window.removeEventListener(\"pointerdown\", onMove);\n      window.removeEventListener(\"pointerup\", onUp);\n      document.documentElement.removeEventListener(\"pointerleave\", onLeave);\n      root.removeAttribute(\"data-gl\");\n      gl.getExtension(\"WEBGL_lose_context\")?.loseContext();\n      canvas.remove();\n    };\n  }, []);\n\n  useEffect(() => { vivo.current?.rebuild(); }, [texto, size, bevel, thickness, intensity, tint, sweep]);\n\n  return (\n    <span ref={rootRef} className={`furin-chrome-text ${className}`} style={{ fontSize: size, letterSpacing: `${(thickness * 0.1 - 0.01).toFixed(3)}em`, ...style }}>\n      <span ref={textRef} className=\"furin-chrome-text__texto\">{texto}</span>\n    </span>\n  );\n}\n\nexport default ChromeText;\n",
      "type": "registry:component",
      "target": "components/furin/chrome-text/ChromeText.tsx"
    },
    {
      "path": "registry/chrome-text/ChromeText.css",
      "content": ".furin-chrome-text {\n  position: relative;\n  display: inline-block;\n  max-width: 100%;\n  font-family: \"Geist\", \"Inter\", \"SF Pro Display\", system-ui, -apple-system, \"Helvetica Neue\", Arial, sans-serif;\n  font-weight: 800;\n  line-height: 1.05;\n  letter-spacing: -0.025em;\n  text-align: center;\n  isolation: isolate;\n}\n/* El texto real sigue en el DOM (se lee, se selecciona, se copia); el cromo lo pinta el lienzo encima. */\n.furin-chrome-text__texto {\n  display: block;\n  white-space: pre;\n  color: transparent;\n}\n/* Mientras WebGL no está listo (o no existe) queda un degradado cromado de CSS. */\n.furin-chrome-text:not([data-gl]) .furin-chrome-text__texto {\n  background: linear-gradient(180deg, #ffffff 0%, #cfd6e0 38%, #4a5160 50%, #aeb8c6 62%, #f4f7fb 100%);\n  -webkit-background-clip: text;\n  background-clip: text;\n}\n.furin-chrome-text__lienzo {\n  position: absolute;\n  pointer-events: none;\n  z-index: 1;\n}\n",
      "type": "registry:file",
      "target": "components/furin/chrome-text/ChromeText.css"
    }
  ]
}
