{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "flow-field",
  "title": "Flow Field",
  "description": "Hundreds of particles ride a seeded noise field, leaving silky fading trails in a seamless loop.",
  "dependencies": ["remotion"],
  "files": [
    {
      "path": "registry/remocn/flow-field/index.tsx",
      "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport {\n  AbsoluteFill,\n  random,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nexport type FlowFieldProps = {\n  /** Number of particles in flight. */\n  particles?: number;\n  /** Trail length in simulation steps (frames). */\n  trailLength?: number;\n  /** Frames for one seamless loop of the field and the particle lifecycles. */\n  loopFrames?: number;\n  /** Particle lifetime in frames, rounded so it divides loopFrames. */\n  lifeFrames?: number;\n  /** Share of particles drawn in the accent color, 0 to 1. */\n  accentRatio?: number;\n  /** Trail width in px at 720p. */\n  strokeWidth?: number;\n  seed?: number;\n  speed?: number;\n  background?: string;\n  color?: string;\n  accent?: string;\n};\n\nexport const flowFieldDefaults: Required<FlowFieldProps> = {\n  particles: 300,\n  trailLength: 72,\n  loopFrames: 300,\n  lifeFrames: 150,\n  accentRatio: 0.1,\n  strokeWidth: 1.4,\n  seed: 1,\n  speed: 1,\n  background: \"#111111\",\n  color: \"#FFFFFF\",\n  accent: \"#E5322D\",\n};\n\nconst TAU = Math.PI * 2;\nconst WAVES = 4;\n// Distance per frame in units of the short side of the frame.\nconst STEP = 0.0042;\nconst BUCKETS = 6;\n\ntype Wave = {\n  fx: number;\n  fy: number;\n  phase: number;\n  drift: number;\n  amp: number;\n};\n\nfunction makeField(seed: number) {\n  const r = (key: string) => random(`flow-field-${seed}-${key}`);\n  const base = r(\"base\") * TAU;\n  const waves: Wave[] = Array.from({ length: WAVES }, (_, k) => ({\n    fx: (r(`fx${k}`) * 2 - 1) * (0.9 + k * 0.8),\n    fy: (r(`fy${k}`) * 2 - 1) * (0.9 + k * 0.8),\n    phase: r(`ph${k}`) * TAU,\n    drift: 0.6 + r(`dr${k}`) * 0.9,\n    amp: 1.6 / (1 + k * 1.4),\n  }));\n  // Flow angle at (x, y). `tau` walks around a circle once per loop, so the\n  // field at the end of the loop is exactly the field at the start.\n  return (x: number, y: number, tau: number) => {\n    let a = base;\n    for (const w of waves) {\n      a +=\n        w.amp *\n        Math.sin(\n          w.fx * x + w.fy * y + w.phase + w.drift * Math.sin(tau + w.phase),\n        );\n    }\n    return a;\n  };\n}\n\nconst fmt = (n: number) => Math.round(n * 10) / 10;\n\nexport function FlowField({\n  particles = flowFieldDefaults.particles,\n  trailLength = flowFieldDefaults.trailLength,\n  loopFrames = flowFieldDefaults.loopFrames,\n  lifeFrames = flowFieldDefaults.lifeFrames,\n  accentRatio = flowFieldDefaults.accentRatio,\n  strokeWidth = flowFieldDefaults.strokeWidth,\n  seed = flowFieldDefaults.seed,\n  speed = flowFieldDefaults.speed,\n  background = flowFieldDefaults.background,\n  color = flowFieldDefaults.color,\n  accent = flowFieldDefaults.accent,\n}: FlowFieldProps) {\n  const frame = useCurrentFrame();\n  const { width, height } = useVideoConfig();\n  const field = useMemo(() => makeField(seed), [seed]);\n\n  const unit = Math.min(width, height);\n  const spanX = width / unit;\n  const spanY = height / unit;\n  const loop = Math.max(1, Math.round(loopFrames));\n  const lives = Math.max(1, Math.round(loop / Math.max(1, lifeFrames)));\n  const life = loop / lives;\n  const trail = Math.max(2, Math.min(trailLength, life * 0.6));\n  const travel = life - trail;\n  const time = frame * speed;\n\n  const paths = {\n    color: new Array<string>(BUCKETS).fill(\"\"),\n    accent: new Array<string>(BUCKETS).fill(\"\"),\n  };\n  const pts: number[] = [];\n\n  for (let i = 0; i < particles; i++) {\n    const offset = random(`flow-field-${seed}-off-${i}`) * life;\n    const local = time + offset;\n    const cycle = Math.floor(local / life);\n    const age = local - cycle * life;\n    // Head moves for `travel` frames, then stops while the tail catches up,\n    // so trails grow in and drain out instead of popping.\n    const head = Math.min(age, travel);\n    const tail = Math.max(0, age - trail);\n    if (head - tail < 0.05) continue;\n\n    const slot = ((cycle % lives) + lives) % lives;\n    const key = `flow-field-${seed}-p${i}-c${slot}`;\n    let x = -0.08 + random(`${key}-x`) * (spanX + 0.16);\n    let y = -0.08 + random(`${key}-y`) * (spanY + 0.16);\n    const birth = time - age;\n\n    pts.length = 0;\n    pts.push(x, y);\n    const steps = Math.ceil(head);\n    for (let s = 0; s < steps; s++) {\n      const a = field(x, y, (TAU * (birth + s)) / loop);\n      x += Math.cos(a) * STEP;\n      y += Math.sin(a) * STEP;\n      pts.push(x, y);\n    }\n    const at = (k: number) => {\n      const j = Math.min(Math.floor(k), steps - 1);\n      const f = k - j;\n      if (j < 0) return [pts[0], pts[1]];\n      return [\n        pts[j * 2] + (pts[j * 2 + 2] - pts[j * 2]) * f,\n        pts[j * 2 + 1] + (pts[j * 2 + 3] - pts[j * 2 + 1]) * f,\n      ];\n    };\n\n    const target =\n      random(`flow-field-${seed}-accent-${i}`) < accentRatio\n        ? paths.accent\n        : paths.color;\n    // Split the visible trail into age buckets; older buckets are fainter.\n    for (let b = 0; b < BUCKETS; b++) {\n      const from = Math.max(tail, age - (trail * (b + 1)) / BUCKETS);\n      const to = Math.min(head, age - (trail * b) / BUCKETS);\n      if (to - from <= 0.01) continue;\n      const [fx, fy] = at(from);\n      let d = `M ${fmt(fx * unit)} ${fmt(fy * unit)}`;\n      for (let k = Math.floor(from) + 1; k < to; k++) {\n        d += ` L ${fmt(pts[k * 2] * unit)} ${fmt(pts[k * 2 + 1] * unit)}`;\n      }\n      const [tx, ty] = at(to);\n      d += ` L ${fmt(tx * unit)} ${fmt(ty * unit)} `;\n      target[b] += d;\n    }\n  }\n\n  const sw = (strokeWidth * unit) / 720;\n  const layers = [\n    { key: \"color\", paint: color, list: paths.color },\n    { key: \"accent\", paint: accent, list: paths.accent },\n  ];\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: background }}>\n      <svg\n        width=\"100%\"\n        height=\"100%\"\n        viewBox={`0 0 ${width} ${height}`}\n        role=\"img\"\n        aria-label=\"Flow field\"\n        style={{ display: \"block\" }}\n      >\n        {layers.map((layer) => (\n          <g\n            key={layer.key}\n            fill=\"none\"\n            stroke={layer.paint}\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n          >\n            {layer.list.map((d, b) => (\n              <path\n                key={b}\n                d={d}\n                strokeOpacity={0.9 * (1 - b / BUCKETS) ** 1.4}\n                strokeWidth={sw * (1 - (b / BUCKETS) * 0.5)}\n              />\n            ))}\n          </g>\n        ))}\n      </svg>\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/flow-field.tsx"
    }
  ],
  "type": "registry:component"
}
