{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "aurora-field",
  "type": "registry:ui",
  "title": "Aurora Field",
  "description": "A fluid colour atmosphere of drifting, blurred light that fills its container.",
  "author": "WANSTUDIO <https://wanstudio.my>",
  "dependencies": [],
  "files": [
    {
      "path": "registry/wancraft/aurora-field.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nexport interface AuroraFieldProps extends React.ComponentPropsWithoutRef<\"div\"> {\n  /** Primary colour (any CSS colour). */\n  colorA?: string;\n  /** Secondary colour. */\n  colorB?: string;\n  /** Depth colour, used for the slow anchoring blob. */\n  colorC?: string;\n  /** Overall strength of the colour field, 0–1. */\n  intensity?: number;\n  /** Blur radius applied to each blob, in px. */\n  blur?: number;\n  /** Playback speed multiplier. 0 freezes the field. */\n  speed?: number;\n}\n\ntype ColorSlot = \"a\" | \"b\" | \"c\";\n\ninterface Blob {\n  color: ColorSlot;\n  /** Position and size in % of the field. */\n  top: number;\n  left: number;\n  width: number;\n  height: number;\n  /** Drift path: [translateX %, translateY %, scale] per keyframe (of the blob's own size). */\n  path: [number, number, number][];\n  /** Base loop duration in ms at speed 1. */\n  duration: number;\n}\n\n// A hand-tuned composition: two large primary masses, one secondary sweep and a deep anchor.\nconst BLOBS: Blob[] = [\n  { color: \"c\", top: 30, left: -20, width: 80, height: 90, duration: 38000, path: [[0, 0, 1], [10, -8, 1.08], [18, 6, 0.96], [4, 10, 1.04]] },\n  { color: \"a\", top: -25, left: -10, width: 62, height: 70, duration: 26000, path: [[0, 0, 1], [22, 14, 1.12], [8, 28, 0.92], [-8, 10, 1.05]] },\n  { color: \"b\", top: -10, left: 45, width: 60, height: 66, duration: 30000, path: [[0, 0, 1], [-18, 16, 0.94], [-30, 4, 1.1], [-10, -10, 1]] },\n  { color: \"a\", top: 52, left: 52, width: 54, height: 58, duration: 34000, path: [[0, 0, 0.9], [-16, -20, 1.06], [6, -30, 1], [12, -8, 0.94]] },\n];\n\nconst subscribeReducedMotion = (onChange: () => void) => {\n  const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n  mq.addEventListener(\"change\", onChange);\n  return () => mq.removeEventListener(\"change\", onChange);\n};\n\n/** SSR-safe `prefers-reduced-motion` subscription. */\nfunction usePrefersReducedMotion() {\n  return React.useSyncExternalStore(\n    subscribeReducedMotion,\n    () => window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches,\n    () => false,\n  );\n}\n\nconst toTransform = ([x, y, s]: [number, number, number]) => `translate3d(${x}%, ${y}%, 0) scale(${s})`;\n\n/**\n * A fluid, slowly drifting colour atmosphere built from blurred radial blobs.\n * Only `transform` is animated (GPU-composited); animation pauses while offscreen or\n * in a hidden tab, and reduced-motion users get a still composition.\n */\nexport function AuroraField({\n  colorA = \"#FF7448\",\n  colorB = \"#B6F3D1\",\n  colorC = \"#0E4F4C\",\n  intensity = 0.7,\n  blur = 72,\n  speed = 1,\n  className = \"\",\n  style,\n  children,\n  ...props\n}: AuroraFieldProps) {\n  const rootRef = React.useRef<HTMLDivElement>(null);\n  const blobRefs = React.useRef<(HTMLDivElement | null)[]>([]);\n  const reduced = usePrefersReducedMotion();\n  const still = reduced || speed <= 0;\n  const animationsRef = React.useRef<Animation[]>([]);\n  const speedRef = React.useRef(speed);\n\n  // Speed changes retime the running loops instead of restarting them.\n  React.useEffect(() => {\n    speedRef.current = speed;\n    for (const a of animationsRef.current) a.updatePlaybackRate(speed);\n  }, [speed]);\n\n  React.useEffect(() => {\n    const root = rootRef.current;\n    if (!root || still) return;\n\n    const animations = blobRefs.current.flatMap((el, i) => {\n      if (!el || typeof el.animate !== \"function\") return [];\n      const blob = BLOBS[i];\n      const keyframes = [...blob.path, blob.path[0]].map((p) => ({ transform: toTransform(p) }));\n      const anim = el.animate(keyframes, {\n        duration: blob.duration,\n        iterations: Infinity,\n        easing: \"ease-in-out\",\n        // Offset each blob so the composition never \"resets\" in sync.\n        delay: -i * 4200,\n      });\n      anim.playbackRate = speedRef.current;\n      return [anim];\n    });\n    animationsRef.current = animations;\n\n    let inView = true;\n    const sync = () => {\n      const run = inView && !document.hidden;\n      for (const a of animations) {\n        if (run && a.playState !== \"running\") a.play();\n        else if (!run && a.playState === \"running\") a.pause();\n      }\n    };\n\n    const io = new IntersectionObserver(([entry]) => {\n      inView = entry.isIntersecting;\n      sync();\n    });\n    io.observe(root);\n    document.addEventListener(\"visibilitychange\", sync);\n\n    return () => {\n      io.disconnect();\n      document.removeEventListener(\"visibilitychange\", sync);\n      for (const a of animations) a.cancel();\n      animationsRef.current = [];\n    };\n  }, [still]);\n\n  const colors: Record<ColorSlot, string> = { a: colorA, b: colorB, c: colorC };\n  const opacity = Math.min(Math.max(intensity, 0), 1);\n\n  return (\n    <div\n      ref={rootRef}\n      className={`absolute inset-0 overflow-hidden ${className}`}\n      style={style}\n      {...props}\n    >\n      <div aria-hidden=\"true\" className=\"pointer-events-none absolute inset-0\">\n        {BLOBS.map((blob, i) => (\n          <div\n            key={i}\n            ref={(el) => {\n              blobRefs.current[i] = el;\n            }}\n            // Screen blending on dark surfaces makes the colour read as light rather than paint.\n            className=\"absolute rounded-full will-change-transform dark:mix-blend-screen\"\n            style={{\n              opacity,\n              top: `${blob.top}%`,\n              left: `${blob.left}%`,\n              width: `${blob.width}%`,\n              height: `${blob.height}%`,\n              // closest-side keeps a soft edge even with little blur, so blur can stay cheap.\n              background: `radial-gradient(closest-side, ${colors[blob.color]}, ${colors[blob.color]} 22%, transparent)`,\n              filter: blur > 0 ? `blur(${blur}px)` : undefined,\n              // A mid-path pose gives the still (reduced-motion) composition some life.\n              transform: still ? toTransform(blob.path[1]) : undefined,\n            }}\n          />\n        ))}\n      </div>\n      {children != null && <div className=\"relative h-full w-full\">{children}</div>}\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "backgrounds"
  ],
  "docs": "Documentation: https://wancraft.my/components/aurora-field"
}
