{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "flow-field",
  "title": "Flow Field",
  "description": "Particles streaming along an evolving noise vector field, leaving silky fading trails. Paints its own themed background.",
  "registryDependencies": ["@godui/godui-theme"],
  "files": [
    {
      "path": "packages/components/src/flow-field/flow-field.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nexport type FlowFieldProps = React.HTMLAttributes<HTMLDivElement> & {\n  /** Number of particles tracing the field. */\n  particleCount?: number;\n  /** Field scale — smaller means broader, smoother currents. */\n  noiseScale?: number;\n  /** Flow speed multiplier. `1` is the calm default. */\n  speed?: number;\n  /**\n   * Trail color, any CSS color string. Defaults to the `--color-primary`\n   * token, re-resolved on theme change.\n   */\n  color?: string;\n  /**\n   * Trail fade per frame, `0`–`1`. Lower leaves longer, silkier trails;\n   * higher keeps the field crisp.\n   */\n  fade?: number;\n};\n\nfunction rgbTriple(input: string): [number, number, number] {\n  if (typeof document === \"undefined\") return [0, 0, 0];\n  try {\n    const c = document.createElement(\"canvas\");\n    c.width = 1;\n    c.height = 1;\n    const ctx = c.getContext(\"2d\", { willReadFrequently: true });\n    if (!ctx) return [0, 0, 0];\n    ctx.fillStyle = input;\n    ctx.fillRect(0, 0, 1, 1);\n    const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;\n    return [r, g, b];\n  } catch {\n    return [0, 0, 0];\n  }\n}\n\n// Compact 2D value noise (hash + smooth interpolation) for the flow angles.\nfunction hash(x: number, y: number): number {\n  const s = Math.sin(x * 127.1 + y * 311.7) * 43758.5453;\n  return s - Math.floor(s);\n}\nfunction valueNoise(x: number, y: number): number {\n  const ix = Math.floor(x);\n  const iy = Math.floor(y);\n  const fx = x - ix;\n  const fy = y - iy;\n  const ux = fx * fx * (3 - 2 * fx);\n  const uy = fy * fy * (3 - 2 * fy);\n  const a = hash(ix, iy);\n  const b = hash(ix + 1, iy);\n  const c = hash(ix, iy + 1);\n  const d = hash(ix + 1, iy + 1);\n  return a + (b - a) * ux + (c - a) * uy + (a - b - c + d) * ux * uy;\n}\n\ntype Particle = { x: number; y: number };\n\n/**\n * A field of particles streaming along an evolving noise vector field, leaving\n * silky fading trails. Paints its own themed background. Drop it as the first\n * child of a `relative` container; your content sits above it.\n */\nconst FlowField = React.forwardRef<HTMLDivElement, FlowFieldProps>(\n  (\n    {\n      className,\n      style,\n      particleCount = 900,\n      noiseScale = 0.0016,\n      speed = 1,\n      color,\n      fade = 0.06,\n      ...props\n    },\n    ref,\n  ) => {\n    const containerRef = React.useRef<HTMLDivElement>(null);\n    const canvasRef = React.useRef<HTMLCanvasElement>(null);\n    const fgRef = React.useRef<[number, number, number]>([0, 0, 0]);\n    const bgRef = React.useRef<[number, number, number]>([255, 255, 255]);\n\n    React.useImperativeHandle(\n      ref,\n      () => containerRef.current as HTMLDivElement,\n    );\n\n    React.useEffect(() => {\n      const container = containerRef.current;\n      if (!container) return;\n      const resolve = () => {\n        fgRef.current = color\n          ? rgbTriple(color)\n          : (() => {\n              const p = document.createElement(\"span\");\n              p.style.cssText =\n                \"position:absolute;width:0;height:0;opacity:0;color:var(--primary)\";\n              container.appendChild(p);\n              const t = rgbTriple(getComputedStyle(p).color);\n              p.remove();\n              return t;\n            })();\n        const pb = document.createElement(\"span\");\n        pb.style.cssText =\n          \"position:absolute;width:0;height:0;opacity:0;background:var(--background)\";\n        container.appendChild(pb);\n        bgRef.current = rgbTriple(getComputedStyle(pb).backgroundColor);\n        pb.remove();\n      };\n      resolve();\n      const observer = new MutationObserver(resolve);\n      observer.observe(document.documentElement, {\n        attributes: true,\n        attributeFilter: [\"class\", \"data-theme\", \"style\"],\n      });\n      return () => observer.disconnect();\n    }, [color]);\n\n    React.useEffect(() => {\n      const container = containerRef.current;\n      const canvas = canvasRef.current;\n      if (!container || !canvas) return;\n      const ctx = (() => {\n        try {\n          return canvas.getContext(\"2d\");\n        } catch {\n          return null;\n        }\n      })();\n      if (!ctx) return;\n\n      const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n      let w = 0;\n      let h = 0;\n      let particles: Particle[] = [];\n      let rafId = 0;\n      let visible = true;\n      let z = 0;\n\n      const setup = () => {\n        w = container.clientWidth;\n        h = container.clientHeight;\n        const dpr = Math.min(window.devicePixelRatio || 1, 2);\n        canvas.width = Math.floor(w * dpr);\n        canvas.height = Math.floor(h * dpr);\n        canvas.style.width = `${w}px`;\n        canvas.style.height = `${h}px`;\n        ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n        const count = Math.max(\n          120,\n          Math.min(particleCount, Math.round((w * h) / 1400)),\n        );\n        particles = Array.from({ length: count }, () => ({\n          x: Math.random() * w,\n          y: Math.random() * h,\n        }));\n        // Paint the base once.\n        const [br, bg, bb] = bgRef.current;\n        ctx.fillStyle = `rgb(${br}, ${bg}, ${bb})`;\n        ctx.fillRect(0, 0, w, h);\n      };\n\n      const step = () => {\n        const [br, bg, bb] = bgRef.current;\n        ctx.fillStyle = `rgba(${br}, ${bg}, ${bb}, ${fade})`;\n        ctx.fillRect(0, 0, w, h);\n        const [fr, fg, fb] = fgRef.current;\n        ctx.strokeStyle = `rgba(${fr}, ${fg}, ${fb}, 0.5)`;\n        ctx.lineWidth = 1;\n        ctx.beginPath();\n        for (const p of particles) {\n          const angle =\n            valueNoise(p.x * noiseScale, p.y * noiseScale + z) * Math.PI * 4;\n          const nx = p.x + Math.cos(angle) * 1.6 * speed;\n          const ny = p.y + Math.sin(angle) * 1.6 * speed;\n          ctx.moveTo(p.x, p.y);\n          ctx.lineTo(nx, ny);\n          p.x = nx;\n          p.y = ny;\n          if (p.x < 0 || p.x > w || p.y < 0 || p.y > h) {\n            p.x = Math.random() * w;\n            p.y = Math.random() * h;\n          }\n        }\n        ctx.stroke();\n        z += 0.0008 * speed;\n      };\n\n      const tick = () => {\n        step();\n        rafId = requestAnimationFrame(tick);\n      };\n      const start = () => {\n        if (rafId || reduced.matches) return;\n        rafId = requestAnimationFrame(tick);\n      };\n      const stop = () => {\n        if (rafId) cancelAnimationFrame(rafId);\n        rafId = 0;\n      };\n\n      setup();\n      if (reduced.matches) {\n        // Render a frozen still by priming a few hundred steps.\n        for (let i = 0; i < 240; i++) step();\n      } else {\n        start();\n      }\n\n      const resizeObserver = new ResizeObserver(() => {\n        setup();\n        if (reduced.matches) for (let i = 0; i < 240; i++) step();\n      });\n      resizeObserver.observe(container);\n\n      const intersectionObserver = new IntersectionObserver(\n        ([entry]) => {\n          visible = entry.isIntersecting;\n          if (visible) start();\n          else stop();\n        },\n        { threshold: 0 },\n      );\n      intersectionObserver.observe(container);\n\n      const onVisibility = () => {\n        if (document.hidden) stop();\n        else if (visible) start();\n      };\n      document.addEventListener(\"visibilitychange\", onVisibility);\n\n      const onReducedChange = () => {\n        if (reduced.matches) {\n          stop();\n          setup();\n          for (let i = 0; i < 240; i++) step();\n        } else if (visible) start();\n      };\n      reduced.addEventListener(\"change\", onReducedChange);\n\n      return () => {\n        stop();\n        resizeObserver.disconnect();\n        intersectionObserver.disconnect();\n        document.removeEventListener(\"visibilitychange\", onVisibility);\n        reduced.removeEventListener(\"change\", onReducedChange);\n      };\n    }, [particleCount, noiseScale, speed, fade]);\n\n    return (\n      <div\n        ref={containerRef}\n        data-slot=\"flow-field\"\n        aria-hidden=\"true\"\n        className={`absolute inset-0 z-base size-full overflow-hidden ${className ?? \"\"}`}\n        style={style}\n        {...props}\n      >\n        <canvas ref={canvasRef} className=\"pointer-events-none size-full\" />\n      </div>\n    );\n  },\n);\nFlowField.displayName = \"FlowField\";\n\nexport { FlowField };\n",
      "type": "registry:ui",
      "target": "components/godui/flow-field.tsx"
    }
  ],
  "type": "registry:ui"
}
