{
  "name": "flow-field",
  "type": "registry:component",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "type": "registry:component",
      "content": "\"use client\";\n\n/**\n * @name: FlowField\n * @description: Canvas particle flow field background — organic noise-driven streams of glowing light.\n * @version: 1.0.0\n * @author: @dorian_baffier\n * @license: MIT\n * @website: https://kokonutui.com\n * @github: https://github.com/kokonut-labs/kokonutui\n */\n\nimport { motion } from \"motion/react\";\nimport type { ReactNode } from \"react\";\nimport { useEffect, useRef } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\ntype ColorTheme = \"aurora\" | \"ember\" | \"ocean\";\ntype ParticleDensity = \"sparse\" | \"medium\" | \"dense\";\n\ninterface Particle {\n  x: number;\n  y: number;\n  speed: number;\n  hue: number;\n  life: number;\n  maxLife: number;\n}\n\ninterface ThemeConfig {\n  hueStart: number;\n  hueRange: number;\n  saturation: number;\n  lightness: number;\n  bg: string;\n  trailAlpha: number;\n}\n\nexport interface FlowFieldProps {\n  className?: string;\n  children?: ReactNode;\n  theme?: ColorTheme;\n  density?: ParticleDensity;\n}\n\n// ─── Constants ────────────────────────────────────────────────────────────────\n\nconst PARTICLE_COUNTS: Record<ParticleDensity, number> = {\n  sparse: 600,\n  medium: 1200,\n  dense: 2000,\n} as const;\n\nconst THEMES: Record<ColorTheme, ThemeConfig> = {\n  aurora: {\n    hueStart: 120,\n    hueRange: 200,\n    saturation: 90,\n    lightness: 62,\n    bg: \"5, 5, 8\",\n    trailAlpha: 0.06,\n  },\n  ember: {\n    hueStart: 0,\n    hueRange: 55,\n    saturation: 95,\n    lightness: 58,\n    bg: \"8, 4, 2\",\n    trailAlpha: 0.07,\n  },\n  ocean: {\n    hueStart: 180,\n    hueRange: 90,\n    saturation: 88,\n    lightness: 60,\n    bg: \"2, 6, 10\",\n    trailAlpha: 0.06,\n  },\n} as const;\n\n// ─── Noise / vector-field ─────────────────────────────────────────────────────\n\n/**\n * Smooth organic 2D noise via a multi-octave trigonometric series.\n * Returns an angle in radians that evolves continuously with time `t`.\n */\nfunction fieldAngle(x: number, y: number, t: number): number {\n  const s = 0.0025;\n  return (\n    Math.sin(x * s + t * 0.0007) * Math.PI +\n    Math.cos(y * s + t * 0.0005) * Math.PI +\n    Math.sin((x + y) * s * 0.6 + t * 0.0009) * Math.PI * 0.6 +\n    Math.cos((x - y) * s * 0.4 + t * 0.0006) * Math.PI * 0.4\n  );\n}\n\n// ─── Default hero content ─────────────────────────────────────────────────────\n\nfunction DefaultContent() {\n  return (\n    <div className=\"relative z-10 flex flex-col items-center justify-center gap-6 px-6 text-center\">\n      <motion.h1\n        animate={{ opacity: 1, y: 0 }}\n        className=\"max-w-3xl font-bold text-5xl text-white leading-[1.08] tracking-tight sm:text-6xl md:text-7xl\"\n        initial={{ opacity: 0, y: 20 }}\n        transition={{ duration: 0.9, delay: 0.38, ease: [0.22, 0.61, 0.36, 1] }}\n      >\n        Chaos finds its\n        <br />\n        <span className=\"bg-gradient-to-r from-emerald-300 via-teal-200 to-violet-300 bg-clip-text text-transparent\">\n          own beauty\n        </span>\n      </motion.h1>\n\n      <motion.p\n        animate={{ opacity: 1, y: 0 }}\n        className=\"max-w-md text-base text-white/50 leading-relaxed\"\n        initial={{ opacity: 0, y: 16 }}\n        transition={{ duration: 0.9, delay: 0.56, ease: \"easeOut\" }}\n      >\n        Thousands of particles drift through an organic noise field, painting\n        luminous trails that shift and spiral endlessly.\n      </motion.p>\n    </div>\n  );\n}\n\n// ─── Main component ───────────────────────────────────────────────────────────\n\nexport default function FlowField({\n  className,\n  children,\n  theme = \"aurora\",\n  density = \"medium\",\n}: FlowFieldProps) {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n\n    const cfg = THEMES[theme];\n    const count = PARTICLE_COUNTS[density];\n    const dpr = window.devicePixelRatio ?? 1;\n\n    let width = 0;\n    let height = 0;\n    let animId = 0;\n    let time = 0;\n    let particles: Particle[] = [];\n\n    const spawnParticle = (): Particle => {\n      const maxLife = 200 + Math.floor(Math.random() * 300);\n      return {\n        x: Math.random() * width,\n        y: Math.random() * height,\n        speed: 1.1 + Math.random() * 1.8,\n        hue: cfg.hueStart + Math.random() * cfg.hueRange,\n        life: Math.floor(Math.random() * maxLife),\n        maxLife,\n      };\n    };\n\n    const resize = () => {\n      width = window.innerWidth;\n      height = window.innerHeight;\n      canvas.width = Math.round(width * dpr);\n      canvas.height = Math.round(height * dpr);\n      canvas.style.width = `${width}px`;\n      canvas.style.height = `${height}px`;\n      ctx.scale(dpr, dpr);\n\n      // Fill dark base on resize\n      ctx.fillStyle = `rgb(${cfg.bg})`;\n      ctx.fillRect(0, 0, width, height);\n\n      // Re-seed particles spread across the canvas\n      particles = Array.from({ length: count }, spawnParticle);\n    };\n\n    const render = () => {\n      time++;\n\n      // Fade previous frame — each dot persists ~16 frames, creating soft trails\n      ctx.fillStyle = `rgba(${cfg.bg}, ${cfg.trailAlpha})`;\n      ctx.fillRect(0, 0, width, height);\n\n      for (const p of particles) {\n        const angle = fieldAngle(p.x, p.y, time);\n\n        p.x += Math.cos(angle) * p.speed;\n        p.y += Math.sin(angle) * p.speed;\n        p.life++;\n\n        // Respawn aged-out particles at a random position\n        if (p.life > p.maxLife) {\n          p.x = Math.random() * width;\n          p.y = Math.random() * height;\n          p.life = 0;\n          p.hue = cfg.hueStart + Math.random() * cfg.hueRange;\n          continue;\n        }\n\n        // Wrap edges\n        if (p.x < 0) p.x += width;\n        else if (p.x > width) p.x -= width;\n        if (p.y < 0) p.y += height;\n        else if (p.y > height) p.y -= height;\n\n        // Fade in / out over particle lifetime\n        const progress = p.life / p.maxLife;\n        const fadeIn = Math.min(progress * 8, 1);\n        const fadeOut = Math.min((1 - progress) * 6, 1);\n        const alpha = fadeIn * fadeOut * 0.9;\n\n        // Hue shifts subtly with field direction for color variety\n        const hueMod = (p.hue + (angle / (Math.PI * 2)) * 70 + 360) % 360;\n\n        ctx.beginPath();\n        ctx.arc(p.x, p.y, 1.3, 0, Math.PI * 2);\n        ctx.fillStyle = `hsla(${hueMod}, ${cfg.saturation}%, ${cfg.lightness}%, ${alpha})`;\n        ctx.fill();\n      }\n\n      animId = requestAnimationFrame(render);\n    };\n\n    resize();\n    window.addEventListener(\"resize\", resize);\n    render();\n\n    return () => {\n      cancelAnimationFrame(animId);\n      window.removeEventListener(\"resize\", resize);\n    };\n  }, [theme, density]);\n\n  const bgColor = THEMES[theme].bg;\n\n  return (\n    <div\n      className={cn(\n        \"relative flex min-h-screen w-full items-center justify-center overflow-hidden\",\n        className\n      )}\n      style={{ background: `rgb(${bgColor})` }}\n    >\n      <canvas\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute inset-0\"\n        ref={canvasRef}\n      />\n\n      {/* Radial vignette — focuses center, dims edges */}\n      <div\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute inset-0\"\n        style={{\n          background: `radial-gradient(ellipse 65% 60% at 50% 50%, transparent 20%, rgba(${bgColor}, 0.92) 100%)`,\n        }}\n      />\n\n      {/* Soft top / bottom fades */}\n      <div\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute inset-x-0 top-0 h-40\"\n        style={{\n          background: `linear-gradient(to bottom, rgb(${bgColor}), transparent)`,\n        }}\n      />\n      <div\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute inset-x-0 bottom-0 h-40\"\n        style={{\n          background: `linear-gradient(to top, rgb(${bgColor}), transparent)`,\n        }}\n      />\n\n      {children ?? <DefaultContent />}\n    </div>\n  );\n}\n",
      "path": "/components/kokonutui/flow-field.tsx",
      "target": "components/kokonutui/flow-field.tsx"
    }
  ]
}