{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "aurora-field",
  "title": "Aurora Field",
  "description": "A drifting gradient backdrop built from the colours already in your theme.",
  "registryDependencies": [
    "utils",
    "https://ui.tinkererslabs.com/r/render-surface.json"
  ],
  "files": [
    {
      "path": "registry/default/aurora-field/aurora-field.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  RenderSurface,\n  useThemeColors,\n  type SurfaceColor,\n} from \"@/registry/default/render-surface/render-surface\"\nimport { cn } from \"@/lib/utils\"\n\nexport type AuroraFieldProps = React.HTMLAttributes<HTMLDivElement> & {\n  /**\n   * Theme custom properties or plain CSS colours. Fewer entries than blobs is\n   * the usual case: the rest are derived by rotating the hue of these.\n   */\n  colors?: readonly string[]\n  blobs?: number\n  speed?: number\n  /** How much of the box each blob covers, as a fraction of its longest edge. */\n  spread?: number\n  opacity?: number\n  paused?: boolean\n  surfaceClassName?: string\n}\n\nconst SPREAD_DEGREES = 44\n\ntype Blob = {\n  color: SurfaceColor\n  radius: number\n  ax: number\n  ay: number\n  fx: number\n  fy: number\n  px: number\n  py: number\n}\n\nfunction rotateHue([r, g, b]: SurfaceColor, degrees: number): SurfaceColor {\n  const max = Math.max(r, g, b)\n  const min = Math.min(r, g, b)\n  const lightness = (max + min) / 2\n  const range = max - min\n\n  if (range === 0) return [r, g, b]\n\n  const saturation =\n    lightness > 0.5 ? range / (2 - max - min) : range / (max + min)\n\n  let hue: number\n  if (max === r) hue = (g - b) / range + (g < b ? 6 : 0)\n  else if (max === g) hue = (b - r) / range + 2\n  else hue = (r - g) / range + 4\n\n  hue = (hue / 6 + degrees / 360 + 1) % 1\n\n  const q =\n    lightness < 0.5\n      ? lightness * (1 + saturation)\n      : lightness + saturation - lightness * saturation\n  const p = 2 * lightness - q\n\n  const channel = (offset: number) => {\n    let value = hue + offset\n    if (value < 0) value += 1\n    if (value > 1) value -= 1\n    if (value < 1 / 6) return p + (q - p) * 6 * value\n    if (value < 1 / 2) return q\n    if (value < 2 / 3) return p + (q - p) * (2 / 3 - value) * 6\n    return p\n  }\n\n  return [channel(1 / 3), channel(0), channel(-1 / 3)]\n}\n\nfunction rgba([r, g, b]: SurfaceColor, alpha: number) {\n  return `rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, ${alpha})`\n}\n\nexport function AuroraField({\n  colors = [\"--primary\"],\n  blobs = 5,\n  speed = 1,\n  spread = 0.55,\n  opacity = 0.85,\n  paused,\n  className,\n  surfaceClassName,\n  children,\n  ...rootProps\n}: AuroraFieldProps) {\n  const rootRef = React.useRef<HTMLDivElement>(null)\n  const tokens = React.useMemo(\n    () => colors.filter((entry) => entry.startsWith(\"--\")),\n    [colors]\n  )\n\n  const resolved = useThemeColors(rootRef, tokens)\n\n  const palette = React.useMemo(() => {\n    const base = colors.map((entry) =>\n      entry.startsWith(\"--\") ? resolved[entry] : undefined\n    )\n\n    return base.filter((entry): entry is SurfaceColor => entry !== undefined)\n  }, [colors, resolved])\n\n  const count = Math.max(1, Math.round(blobs))\n  const ready = palette.length > 0\n\n  const setup = React.useCallback(() => {\n    const made: Blob[] = []\n\n    for (let index = 0; index < count; index += 1) {\n      const source = palette[index % palette.length] ?? ([0, 0, 0] as const)\n\n      // Derived shades stay in a narrow band around the colour they came from.\n      // Spread any wider and a single tomato token produces a rainbow, which is\n      // the opposite of inheriting the theme.\n      const offset =\n        index < palette.length\n          ? 0\n          : (index / Math.max(count - 1, 1) - 0.5) * SPREAD_DEGREES\n\n      made.push({\n        color: offset === 0 ? source : rotateHue(source, offset),\n        radius: spread * (0.7 + ((index * 0.37) % 0.6)),\n        ax: 0.18 + ((index * 0.23) % 0.22),\n        ay: 0.16 + ((index * 0.31) % 0.2),\n        fx: 0.07 + index * 0.017,\n        fy: 0.055 + index * 0.021,\n        px: index * 1.7,\n        py: index * 2.3,\n      })\n    }\n\n    return made\n  }, [count, palette, spread])\n\n  const draw = React.useCallback(\n    ({\n      context,\n      size,\n      state,\n      time,\n    }: {\n      context: CanvasRenderingContext2D\n      size: { width: number; height: number; dpr: number }\n      state: Blob[]\n      time: number\n    }) => {\n      const { width, height, dpr } = size\n      const longest = Math.max(width, height)\n\n      context.setTransform(dpr, 0, 0, dpr, 0, 0)\n      context.clearRect(0, 0, width, height)\n\n      for (const blob of state) {\n        const t = time * speed\n        const x =\n          width * (0.5 + blob.ax * Math.sin(t * blob.fx * 6.28 + blob.px))\n        const y =\n          height * (0.5 + blob.ay * Math.cos(t * blob.fy * 6.28 + blob.py))\n        const radius = longest * blob.radius\n\n        const gradient = context.createRadialGradient(x, y, 0, x, y, radius)\n        gradient.addColorStop(0, rgba(blob.color, opacity))\n        gradient.addColorStop(1, rgba(blob.color, 0))\n\n        context.fillStyle = gradient\n        context.fillRect(0, 0, width, height)\n      }\n    },\n    [opacity, speed]\n  )\n\n  return (\n    <div\n      ref={rootRef}\n      data-slot=\"aurora-field\"\n      className={cn(\n        \"bg-background relative isolate overflow-hidden\",\n        className\n      )}\n      {...rootProps}\n    >\n      {ready && (\n        <RenderSurface<Blob[], \"2d\">\n          setup={setup}\n          draw={draw}\n          paused={paused}\n          maxDpr={1}\n          className={cn(\n            \"pointer-events-none absolute inset-0 -z-10 scale-125 blur-3xl\",\n            surfaceClassName\n          )}\n        />\n      )}\n      {children}\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "scene",
    "background",
    "gradient",
    "motion"
  ],
  "type": "registry:ui"
}