{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "aurora",
  "title": "Aurora",
  "description": "Minimal aurora borealis light - fanned curtains in tones of a single green, drifting slowly across a deep night sky.",
  "files": [
    {
      "path": "components/backgrounds/AuroraBackground.tsx",
      "content": "'use client';\n\nimport { useEffect, useRef, type ReactNode } from 'react';\n\n// ---------------------------------------------------------------------------\n// Props\n// ---------------------------------------------------------------------------\n//\n// children           Content layered above the aurora (headline, CTA, etc.)\n// className          Styles for the content wrapper\n// containerClassName Styles for the outer shell (size, layout)\n// theme              \"dark\" (default) | \"light\"\n// colors             Aurora tones, sampled left → right across the sky.\n//                    Keep these close in hue — one color's shades reads as\n//                    a real aurora; a rainbow does not. Defaults per theme.\n// intensity          Overall brightness of the light\n// speed              \"slow\" (default) | \"medium\" | \"fast\"\n//\ntype AuroraBackgroundProps = {\n  children?: ReactNode;\n  className?: string;\n  containerClassName?: string;\n  theme?: 'dark' | 'light';\n  colors?: string[];\n  intensity?: number;\n  speed?: 'slow' | 'medium' | 'fast';\n};\n\nconst THEMES = {\n  dark: {\n    sky: '#04030f',\n    colors: ['#0f766e', '#15916b', '#34d399', '#6ee7b7', '#22c55e'],\n    blend: 'screen' as const,\n  },\n  light: {\n    sky: '#f2f6f4',\n    colors: ['#0d9488', '#059669', '#10b981', '#34d399', '#16a34a'],\n    blend: 'multiply' as const,\n  },\n};\n\nconst SPEED_STEP = { slow: 0.006, medium: 0.01, fast: 0.018 } as const;\n\nconst randFrom = (n: number) => {\n  let seed = n;\n  return () => {\n    seed = (seed * 9301 + 49297) % 233280;\n    return seed / 233280;\n  };\n};\n\ntype Band = {\n  kind: 'haze' | 'curtain';\n  y0: number;\n  y1: number;\n  colorT: number;\n  f1: number;\n  f2: number;\n  f3: number;\n  s1: number;\n  s2: number;\n  s3: number;\n  p1: number;\n  p2: number;\n  p3: number;\n  a1: number;\n  a2: number;\n  a3: number;\n  alpha: number;\n  folds: { u: number; w: number; p: number; s: number }[];\n};\n\nconst BANDS: Band[] = (() => {\n  const rand = randFrom(31);\n  const bands: Band[] = [];\n\n  for (let i = 0; i < 2; i++) {\n    bands.push({\n      kind: 'haze',\n      y0: 0.2 + rand() * 0.08,\n      y1: 0.6 + rand() * 0.1,\n      colorT: 0.3 + i * 0.35,\n      f1: 2.2 + rand(),\n      f2: 4 + rand() * 1.6,\n      f3: 0.9 + rand() * 0.5,\n      s1: 0.3 + rand() * 0.2,\n      s2: 0.2 + rand() * 0.15,\n      s3: 0.12 + rand() * 0.1,\n      p1: rand() * Math.PI * 2,\n      p2: rand() * Math.PI * 2,\n      p3: rand() * Math.PI * 2,\n      a1: 22 + rand() * 12,\n      a2: 14 + rand() * 10,\n      a3: 18 + rand() * 10,\n      alpha: 0.32 + rand() * 0.1,\n      folds: [],\n    });\n  }\n\n  for (let i = 0; i < 3; i++) {\n    bands.push({\n      kind: 'curtain',\n      y0: 0.16 + rand() * 0.12,\n      y1: 0.54 + rand() * 0.16,\n      colorT: Math.min(1, Math.max(0, i / 2 + (rand() - 0.5) * 0.15)),\n      f1: 3.2 + rand() * 2.4,\n      f2: 6.5 + rand() * 3,\n      f3: 1.2 + rand(),\n      s1: 0.5 + rand() * 0.35,\n      s2: 0.32 + rand() * 0.28,\n      s3: 0.18 + rand() * 0.16,\n      p1: rand() * Math.PI * 2,\n      p2: rand() * Math.PI * 2,\n      p3: rand() * Math.PI * 2,\n      a1: 36 + rand() * 22,\n      a2: 22 + rand() * 16,\n      a3: 28 + rand() * 18,\n      alpha: 0.5 + rand() * 0.22,\n      folds: Array.from({ length: 5 + Math.floor(rand() * 3) }, () => ({\n        u: rand(),\n        w: 0.06 + rand() * 0.1,\n        p: rand() * Math.PI * 2,\n        s: 0.25 + rand() * 0.45,\n      })),\n    });\n  }\n\n  return bands;\n})();\n\nconst hexA = (hex: string, a: number) => {\n  const n = hex.replace('#', '');\n  const r = parseInt(n.slice(0, 2), 16);\n  const g = parseInt(n.slice(2, 4), 16);\n  const b = parseInt(n.slice(4, 6), 16);\n  return `rgba(${r},${g},${b},${Math.max(0, Math.min(1, a))})`;\n};\n\nconst pick = (palette: string[], t: number) =>\n  palette[Math.round(Math.min(1, Math.max(0, t)) * (palette.length - 1))];\n\nconst ribbonY = (\n  u: number,\n  t: number,\n  band: Band,\n  edge: 'top' | 'bot',\n  h: number\n) => {\n  const wave =\n    Math.sin(u * band.f1 + t * band.s1 + band.p1) * band.a1 +\n    Math.sin(u * band.f2 - t * band.s2 + band.p2) * band.a2 +\n    Math.sin(u * band.f3 + t * band.s3 + band.p3) * band.a3;\n  const base = (edge === 'top' ? band.y0 : band.y1) * h;\n  return base + wave * (edge === 'top' ? 1 : 1.3);\n};\n\nexport default function AuroraBackground({\n  children,\n  className = '',\n  containerClassName = '',\n  theme = 'dark',\n  colors,\n  intensity = 1,\n  speed = 'medium',\n}: AuroraBackgroundProps) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const rafRef = useRef(0);\n  const timeRef = useRef(0);\n\n  const themeTokens = THEMES[theme] ?? THEMES.dark;\n  const palette = colors ?? themeTokens.colors;\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    const container = containerRef.current;\n    if (!canvas || !container) return;\n\n    const ctx = canvas.getContext('2d', { alpha: false });\n    if (!ctx) return;\n\n    const step = SPEED_STEP[speed] ?? SPEED_STEP.medium;\n    const prefersReducedMotion = window.matchMedia(\n      '(prefers-reduced-motion: reduce)'\n    ).matches;\n\n    // Half-res + CSS blur is the bloom. Never use ctx.filter (it stalls the tab).\n    const SCALE = 0.5;\n    let width = 0;\n    let height = 0;\n\n    const resize = () => {\n      const rect = container.getBoundingClientRect();\n      width = Math.max(1, Math.floor(rect.width * SCALE));\n      height = Math.max(1, Math.floor(rect.height * SCALE));\n      canvas.width = width;\n      canvas.height = height;\n    };\n\n    const traceRibbon = (band: Band, t: number) => {\n      ctx.beginPath();\n      const stepX = 10;\n      for (let x = -20; x <= width + 20; x += stepX) {\n        const y = ribbonY(x / width, t, band, 'top', height);\n        if (x === -20) ctx.moveTo(x, y);\n        else ctx.lineTo(x, y);\n      }\n      for (let x = width + 20; x >= -20; x -= stepX) {\n        ctx.lineTo(x, ribbonY(x / width, t, band, 'bot', height));\n      }\n      ctx.closePath();\n    };\n\n    const drawRibbon = (band: Band, t: number) => {\n      const color = pick(palette, band.colorT);\n      const a = band.alpha * intensity;\n      const g = ctx.createLinearGradient(\n        0,\n        band.y0 * height - 20,\n        0,\n        band.y1 * height + 24\n      );\n      const body = band.kind === 'haze' ? a : a * 0.4;\n      g.addColorStop(0, hexA(color, 0));\n      g.addColorStop(0.22, hexA(color, body * 0.3));\n      g.addColorStop(0.48, hexA(color, body));\n      g.addColorStop(0.8, hexA(color, body * 0.35));\n      g.addColorStop(1, hexA(color, 0));\n\n      traceRibbon(band, t);\n      ctx.fillStyle = g;\n      ctx.fill();\n\n      if (band.kind !== 'curtain') return;\n\n      ctx.save();\n      traceRibbon(band, t);\n      ctx.clip();\n      for (const fold of band.folds) {\n        const x =\n          (fold.u + Math.sin(t * fold.s + fold.p) * 0.04) * width;\n        const pulse = 0.55 + 0.45 * Math.sin(t * fold.s * 1.4 + fold.p);\n        const fw = fold.w * width;\n        const fg = ctx.createLinearGradient(x - fw, 0, x + fw, 0);\n        fg.addColorStop(0, hexA(color, 0));\n        fg.addColorStop(0.5, hexA(color, a * 0.55 * pulse));\n        fg.addColorStop(1, hexA(color, 0));\n        ctx.fillStyle = fg;\n        ctx.fillRect(x - fw, 0, fw * 2, height);\n      }\n      ctx.restore();\n    };\n\n    const draw = () => {\n      timeRef.current += prefersReducedMotion ? 0 : step;\n      const t = timeRef.current;\n\n      ctx.globalCompositeOperation = 'source-over';\n      ctx.fillStyle = themeTokens.sky;\n      ctx.fillRect(0, 0, width, height);\n\n      ctx.globalCompositeOperation = themeTokens.blend;\n      for (const band of BANDS) drawRibbon(band, t);\n      ctx.globalCompositeOperation = 'source-over';\n\n      if (!prefersReducedMotion) {\n        rafRef.current = requestAnimationFrame(draw);\n      }\n    };\n\n    resize();\n    draw();\n\n    const observer = new ResizeObserver(resize);\n    observer.observe(container);\n\n    return () => {\n      cancelAnimationFrame(rafRef.current);\n      observer.disconnect();\n    };\n  }, [intensity, palette, speed, themeTokens.blend, themeTokens.sky]);\n\n  return (\n    <div\n      ref={containerRef}\n      className={[\n        'relative flex min-h-[480px] w-full flex-col items-center justify-center overflow-hidden',\n        containerClassName,\n      ]\n        .filter(Boolean)\n        .join(' ')}\n      style={{ backgroundColor: themeTokens.sky }}\n    >\n      <canvas\n        ref={canvasRef}\n        className='absolute inset-0 z-0 h-full w-full scale-110 blur-[14px]'\n        aria-hidden\n      />\n      <div\n        className={['relative z-10 px-6 text-center', className]\n          .filter(Boolean)\n          .join(' ')}\n      >\n        {children}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/backgrounds/AuroraBackground.tsx"
    }
  ],
  "categories": [
    "backgrounds"
  ],
  "type": "registry:component"
}