{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "aurora",
  "type": "registry:component",
  "title": "Aurora",
  "description": "A WebGL background of slowly rippling aurora curtains, in your own colors.",
  "author": "Marcelo Treviño Velázquez <https://furin.dev>",
  "dependencies": [],
  "categories": [
    "Backgrounds"
  ],
  "docs": "Installed in components/furin/aurora/. Docs, live controls and props table: https://furin.dev/components/aurora/",
  "files": [
    {
      "path": "registry/aurora/Aurora.tsx",
      "content": "\"use client\";\nimport { useEffect, useRef, useState } from \"react\";\nimport type { CSSProperties, ReactNode } from \"react\";\nimport \"./Aurora.css\";\n\nexport interface AuroraProps {\n  /** Color de la cortina más baja (cualquier color CSS, sin variables). */\n  color1?: string;\n  /** Color de la cortina intermedia. */\n  color2?: string;\n  /** Color de la cortina más alta. */\n  color3?: string;\n  /** Color del cielo que hay detrás de las cortinas. */\n  background?: string;\n  /** Velocidad de la ondulación: 1 es lenta y relajada, 0 la congela. */\n  speed?: number;\n  /** Brillo de las cortinas: menos de 1 las apaga, más de 1 las enciende. */\n  intensity?: number;\n  /** Cuánto se agitan las cortinas hacia arriba y abajo. */\n  amplitude?: number;\n  /** Clase extra para el contenedor. */\n  className?: string;\n  /** Estilos extra para el contenedor. */\n  style?: CSSProperties;\n  /** Contenido que se dibuja encima de la aurora, centrado. */\n  children?: ReactNode;\n}\n\ntype Rgb = [number, number, number];\n\n/** Valores que el bucle de dibujo lee en cada cuadro (así cambiar una prop no reinicia WebGL). */\ninterface LiveValues {\n  colors: [Rgb, Rgb, Rgb];\n  background: Rgb;\n  speed: number;\n  intensity: number;\n  amplitude: number;\n}\n\n/** Con la aurora quieta (movimiento reducido) se muestra este instante, que es uno bonito. */\nconst STILL_TIME = 21;\n/** La aurora es suave: más resolución que esto solo gasta batería. */\nconst MAX_DPR = 1.5;\n\n// ───────────────────────── Shaders ─────────────────────────\n\nconst VERTEX_SHADER = `\nattribute vec2 aPos;\nvoid main() {\n  gl_Position = vec4(aPos, 0.0, 1.0);\n}`;\n\nconst FRAGMENT_SHADER = `\n#ifdef GL_FRAGMENT_PRECISION_HIGH\nprecision highp float;\n#else\nprecision mediump float;\n#endif\n\nuniform vec2 uRes;\nuniform float uTime;\nuniform float uIntensity;\nuniform float uAmplitude;\nuniform vec3 uBg;\nuniform vec3 uColor0;\nuniform vec3 uColor1;\nuniform vec3 uColor2;\n\n// Dirección pseudoaleatoria para cada vértice de la rejilla simplex.\nvec2 hash2(vec2 p) {\n  p = vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3)));\n  return -1.0 + 2.0 * fract(sin(p) * 43758.5453);\n}\n\n// Ruido simplex 2D: devuelve valores suaves entre -1 y 1.\nfloat simplex(vec2 p) {\n  const float F2 = 0.3660254;  // (sqrt(3) - 1) / 2\n  const float G2 = 0.2113249;  // (3 - sqrt(3)) / 6\n  vec2 cell = floor(p + (p.x + p.y) * F2);\n  vec2 a = p - cell + (cell.x + cell.y) * G2;\n  vec2 step1 = a.x > a.y ? vec2(1.0, 0.0) : vec2(0.0, 1.0);\n  vec2 b = a - step1 + G2;\n  vec2 c = a - 1.0 + 2.0 * G2;\n  vec3 w = max(0.5 - vec3(dot(a, a), dot(b, b), dot(c, c)), 0.0);\n  w = w * w * w * w;\n  vec3 g = vec3(\n    dot(a, hash2(cell)),\n    dot(b, hash2(cell + step1)),\n    dot(c, hash2(cell + 1.0))\n  );\n  return 70.0 * dot(w, g);\n}\n\n// Ruido fractal: suma capas de simplex, cada una más fina y más débil.\nfloat fbm(vec2 p) {\n  float sum = 0.0;\n  float gain = 0.5;\n  for (int i = 0; i < 4; i++) {\n    sum += gain * simplex(p);\n    p = p * 2.03 + vec2(11.7, 5.3);\n    gain *= 0.5;\n  }\n  return sum;\n}\n\n// Una cortina: borde ondulante abajo, luz que se desvanece hacia arriba y rayos verticales.\nfloat curtain(vec2 uv, float aspect, float t, float seed, float base, float reach) {\n  float x = uv.x * aspect;\n  float wave = fbm(vec2(x * 0.75 + seed * 7.0, t * 0.16 + seed));\n  float edge = base + wave * 0.34 * uAmplitude;\n  float d = uv.y - edge;\n\n  // Debajo del borde la luz muere rápido; arriba se apaga despacio.\n  float fall = d < 0.0 ? exp(d * 17.0) : exp(-d / reach);\n\n  // Rayos: ruido muy estirado en vertical, más marcado mientras más arriba.\n  float rays = simplex(vec2(x * 4.5 + wave * 1.6 + seed * 13.0, t * 0.22 + d * 0.9)) * 0.5 + 0.5;\n  rays = mix(0.8, 0.12 + 1.7 * rays * rays, smoothstep(-0.02, 0.3, d));\n\n  // Respiración lenta de toda la cortina.\n  float pulse = 0.78 + 0.22 * sin(t * 0.5 + seed * 6.0);\n  return fall * rays * pulse;\n}\n\nvoid main() {\n  vec2 uv = gl_FragCoord.xy / uRes;\n  float aspect = uRes.x / uRes.y;\n\n  float a0 = curtain(uv, aspect, uTime, 0.0, 0.30, 0.20);\n  float a1 = curtain(uv, aspect, uTime * 1.15, 1.7, 0.47, 0.19);\n  float a2 = curtain(uv, aspect, uTime * 0.85, 3.4, 0.63, 0.17);\n\n  vec3 light = (uColor0 * a0 + uColor1 * a1 + uColor2 * a2) * uIntensity;\n\n  // Mezcla tipo \"screen\": la luz suma sin pasarse de blanco y conserva el tono.\n  vec3 col = 1.0 - (1.0 - uBg) * exp(-light * 1.4);\n\n  // Ruido mínimo para que los degradados oscuros no se vean en escalones.\n  float dither = fract(sin(dot(gl_FragCoord.xy, vec2(12.9898, 78.233))) * 43758.5453);\n  col += (dither - 0.5) / 255.0;\n\n  gl_FragColor = vec4(col, 1.0);\n}`;\n\n// ───────────────────────── Color ─────────────────────────\n\nlet colorContext: CanvasRenderingContext2D | null = null;\n\n/** Convierte cualquier color CSS a [r, g, b] entre 0 y 1 (el navegador hace el trabajo). */\nfunction toRgb(color: string): Rgb {\n  colorContext ??= document.createElement(\"canvas\").getContext(\"2d\");\n  if (!colorContext) return [0, 0, 0];\n  colorContext.fillStyle = \"#000000\";\n  colorContext.fillStyle = color; // si el color no es válido se queda en negro\n  const normalized = colorContext.fillStyle; // \"#rrggbb\" o \"rgba(r, g, b, a)\"\n  if (normalized.startsWith(\"#\")) {\n    const n = parseInt(normalized.slice(1), 16);\n    return [(n >> 16) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];\n  }\n  const [r, g, b] = normalized.match(/[\\d.]+/g)!.map(Number);\n  return [r / 255, g / 255, b / 255];\n}\n\n// ───────────────────────── WebGL ─────────────────────────\n\ninterface Renderer {\n  resize(width: number, height: number): void;\n  draw(time: number, values: LiveValues): void;\n  dispose(): void;\n}\n\nfunction compile(gl: WebGLRenderingContext, type: number, source: string): WebGLShader | null {\n  const shader = gl.createShader(type);\n  if (!shader) return null;\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/** Crea el contexto y el programa. Devuelve null si el navegador no puede con WebGL. */\nfunction createRenderer(canvas: HTMLCanvasElement): Renderer | null {\n  const options: WebGLContextAttributes = { alpha: false, antialias: false, depth: false, stencil: false };\n  const gl = (canvas.getContext(\"webgl\", options) ?? canvas.getContext(\"experimental-webgl\", options)) as WebGLRenderingContext | null;\n  if (!gl) return null;\n\n  const uniformNames = [\"uRes\", \"uTime\", \"uIntensity\", \"uAmplitude\", \"uBg\", \"uColor0\", \"uColor1\", \"uColor2\"] as const;\n  let program: WebGLProgram | null = null;\n  let uniforms = {} as Record<(typeof uniformNames)[number], WebGLUniformLocation | null>;\n  let buffer: WebGLBuffer | null = null;\n\n  /** Compila los shaders y sube un triángulo que cubre toda la pantalla. */\n  const build = (): boolean => {\n    const vertex = compile(gl, gl.VERTEX_SHADER, VERTEX_SHADER);\n    const fragment = compile(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER);\n    program = gl.createProgram();\n    if (!vertex || !fragment || !program) return false;\n    gl.attachShader(program, vertex);\n    gl.attachShader(program, fragment);\n    gl.linkProgram(program);\n    gl.deleteShader(vertex);\n    gl.deleteShader(fragment);\n    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) return false;\n\n    gl.useProgram(program);\n    uniforms = Object.fromEntries(uniformNames.map((name) => [name, gl.getUniformLocation(program!, name)])) as typeof uniforms;\n\n    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 position = gl.getAttribLocation(program, \"aPos\");\n    gl.enableVertexAttribArray(position);\n    gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);\n    return true;\n  };\n\n  let ready = build();\n  if (!ready) return null;\n\n  // Si el navegador pierde el contexto (p. ej. demasiados WebGL abiertos) se reconstruye al volver.\n  const onLost = (event: Event) => {\n    event.preventDefault();\n    ready = false;\n  };\n  const onRestored = () => {\n    ready = build();\n  };\n  canvas.addEventListener(\"webglcontextlost\", onLost);\n  canvas.addEventListener(\"webglcontextrestored\", onRestored);\n\n  return {\n    resize(width, height) {\n      canvas.width = width;\n      canvas.height = height;\n    },\n    draw(time, values) {\n      if (!ready || gl.isContextLost()) return;\n      gl.viewport(0, 0, canvas.width, canvas.height);\n      gl.uniform2f(uniforms.uRes, canvas.width, canvas.height);\n      gl.uniform1f(uniforms.uTime, time);\n      gl.uniform1f(uniforms.uIntensity, values.intensity);\n      gl.uniform1f(uniforms.uAmplitude, values.amplitude);\n      gl.uniform3fv(uniforms.uBg, values.background);\n      gl.uniform3fv(uniforms.uColor0, values.colors[0]);\n      gl.uniform3fv(uniforms.uColor1, values.colors[1]);\n      gl.uniform3fv(uniforms.uColor2, values.colors[2]);\n      gl.drawArrays(gl.TRIANGLES, 0, 3);\n    },\n    dispose() {\n      canvas.removeEventListener(\"webglcontextlost\", onLost);\n      canvas.removeEventListener(\"webglcontextrestored\", onRestored);\n      if (program) gl.deleteProgram(program);\n      if (buffer) gl.deleteBuffer(buffer);\n      // Libera la memoria de la GPU de inmediato en lugar de esperar al recolector de basura.\n      gl.getExtension(\"WEBGL_lose_context\")?.loseContext();\n    },\n  };\n}\n\n// ───────────────────────── Componente ─────────────────────────\n\n/** Fondo de cortinas de aurora que ondulan lento (WebGL). Llena a su padre, que debe tener position: relative. */\nexport function Aurora({\n  color1 = \"#2BFFB0\",\n  color2 = \"#2D7BFF\",\n  color3 = \"#A45CFF\",\n  background = \"#0B0B0F\",\n  speed = 1,\n  intensity = 1,\n  amplitude = 1,\n  className = \"\",\n  style,\n  children,\n}: AuroraProps) {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const hostRef = useRef<HTMLDivElement>(null);\n  // \"pending\": aún no se sabe si hay WebGL (el servidor y el primer render pintan el fondo estático).\n  const [mode, setMode] = useState<\"pending\" | \"webgl\" | \"static\">(\"pending\");\n  const live = useRef<LiveValues>({ colors: [[0, 0, 0], [0, 0, 0], [0, 0, 0]], background: [0, 0, 0], speed, intensity, amplitude });\n  const redraw = useRef<() => void>(() => {});\n\n  // Pasa las props actuales al bucle de dibujo.\n  useEffect(() => {\n    live.current = {\n      colors: [toRgb(color1), toRgb(color2), toRgb(color3)],\n      background: toRgb(background),\n      speed,\n      intensity,\n      amplitude,\n    };\n    redraw.current();\n  }, [color1, color2, color3, background, speed, intensity, amplitude]);\n\n  useEffect(() => {\n    const root = rootRef.current;\n    const host = hostRef.current;\n    if (!root || !host) return;\n\n    // El canvas se crea aquí (no en el JSX) para que un nuevo montaje siempre estrene su propio contexto.\n    const canvas = document.createElement(\"canvas\");\n    canvas.className = \"furin-aurora__canvas\";\n    const renderer = createRenderer(canvas);\n    if (!renderer) {\n      setMode(\"static\");\n      return;\n    }\n    host.appendChild(canvas);\n    setMode(\"webgl\");\n\n    const reducedMotion = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    let time = STILL_TIME;\n    let lastNow = 0;\n    let frameId = 0;\n    let visible = true;\n\n    const draw = () => renderer.draw(time, live.current);\n    redraw.current = draw;\n\n    const frame = (now: number) => {\n      frameId = requestAnimationFrame(frame);\n      const elapsed = Math.min((now - lastNow) / 1000, 0.1);\n      lastNow = now;\n      time += elapsed * live.current.speed;\n      draw();\n    };\n    const start = () => {\n      if (frameId || !visible || reducedMotion.matches) return;\n      lastNow = performance.now();\n      frameId = requestAnimationFrame(frame);\n    };\n    const stop = () => {\n      cancelAnimationFrame(frameId);\n      frameId = 0;\n    };\n\n    const resize = () => {\n      const dpr = Math.min(window.devicePixelRatio || 1, MAX_DPR);\n      renderer.resize(Math.max(1, Math.round(root.clientWidth * dpr)), Math.max(1, Math.round(root.clientHeight * dpr)));\n      draw();\n    };\n\n    const resizeObserver = new ResizeObserver(resize);\n    resizeObserver.observe(root);\n\n    // Fuera de pantalla no se gasta GPU.\n    const visibilityObserver = new IntersectionObserver(([entry]) => {\n      visible = entry.isIntersecting;\n      if (visible) start();\n      else stop();\n    });\n    visibilityObserver.observe(root);\n\n    const onMotionPreferenceChange = () => {\n      if (reducedMotion.matches) stop();\n      else start();\n    };\n    reducedMotion.addEventListener(\"change\", onMotionPreferenceChange);\n\n    resize();\n    start();\n\n    return () => {\n      stop();\n      resizeObserver.disconnect();\n      visibilityObserver.disconnect();\n      reducedMotion.removeEventListener(\"change\", onMotionPreferenceChange);\n      redraw.current = () => {};\n      renderer.dispose();\n      canvas.remove();\n    };\n  }, []);\n\n  const colorVars = { \"--furin-aurora-c1\": color1, \"--furin-aurora-c2\": color2, \"--furin-aurora-c3\": color3, \"--furin-aurora-bg\": background } as CSSProperties;\n\n  return (\n    <div ref={rootRef} className={`furin-aurora ${className}`.trim()} style={{ ...colorVars, ...style }}>\n      {/* Sin WebGL (o antes de saberlo) se ve un fondo estático con los mismos colores. */}\n      {mode !== \"webgl\" && <div className=\"furin-aurora__fallback\" />}\n      <div ref={hostRef} className=\"furin-aurora__host\" />\n      {children !== undefined && <div className=\"furin-aurora__content\">{children}</div>}\n    </div>\n  );\n}\n\nexport default Aurora;\n",
      "type": "registry:component",
      "target": "components/furin/aurora/Aurora.tsx"
    },
    {
      "path": "registry/aurora/Aurora.css",
      "content": ".furin-aurora {\n  position: absolute;\n  inset: 0;\n  overflow: hidden;\n  background: var(--furin-aurora-bg, #0b0b0f);\n  color: #fff;\n  isolation: isolate;\n  /* Es un fondo: no estorba al resto de la página. */\n  pointer-events: none;\n}\n\n/* Contenedor del canvas WebGL: lo llena por completo. */\n.furin-aurora__host,\n.furin-aurora__canvas {\n  position: absolute;\n  inset: 0;\n  width: 100%;\n  height: 100%;\n  display: block;\n}\n\n/* Fondo estático para cuando no hay WebGL: los mismos colores, sin movimiento. */\n.furin-aurora__fallback {\n  position: absolute;\n  inset: 0;\n  background:\n    radial-gradient(ellipse 75% 34% at 22% 66%, color-mix(in srgb, var(--furin-aurora-c1) 55%, transparent), transparent 72%),\n    radial-gradient(ellipse 70% 32% at 58% 50%, color-mix(in srgb, var(--furin-aurora-c2) 50%, transparent), transparent 72%),\n    radial-gradient(ellipse 60% 28% at 84% 36%, color-mix(in srgb, var(--furin-aurora-c3) 50%, transparent), transparent 72%),\n    var(--furin-aurora-bg, #0b0b0f);\n}\n\n/* El contenido va encima, centrado. */\n.furin-aurora__content {\n  position: relative;\n  z-index: 1;\n  width: 100%;\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  text-align: center;\n}\n\n/* Lo que pongas encima sí recibe clics y toques. */\n.furin-aurora__content > * {\n  pointer-events: auto;\n}\n",
      "type": "registry:file",
      "target": "components/furin/aurora/Aurora.css"
    }
  ]
}
