{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "boids-ecosystem",
  "type": "registry:block",
  "title": "Boids ecosystem",
  "description": "Boids ecosystem",
  "files": [
    {
      "path": "components/usages/boidsecosystemusage.tsx",
      "content": "import BoidsEcosystem from \"@/registry/open-source/boids-ecosystem\";\n\nexport default function Usage() {\n    return (\n        <div className=\"h-screen w-full flex items-center justify-center relative overflow-hidden bg-background\">\n            <BoidsEcosystem />\n        </div>\n    );\n}",
      "type": "registry:block",
      "target": "~/example.tsx"
    },
    {
      "path": "components/usages/boidsecosystemusage.tsx",
      "content": "import BoidsEcosystem from \"@/registry/open-source/boids-ecosystem\";\n\nexport default function Usage() {\n    return (\n        <div className=\"h-screen w-full flex items-center justify-center relative overflow-hidden bg-background\">\n            <BoidsEcosystem />\n        </div>\n    );\n}",
      "type": "registry:ui"
    },
    {
      "path": "registry/open-source/boids-ecosystem.tsx",
      "content": "\"use client\";\n\nimport { type RefObject, useEffect, useRef } from \"react\";\n\nimport { cn } from \"@/registry/utilities/cn\";\n\nexport interface BoidAgent {\n    x: number;\n    y: number;\n    vx: number;\n    vy: number;\n    color: string;\n}\n\n/**\n * Force point that pulls boids toward (x, y). Coordinates are normalized 0..1\n * across the canvas, so callers don't need to know the canvas pixel size.\n */\nexport interface BoidAttractor {\n    x: number;\n    y: number;\n    /** 0 = no pull, 0.05 = gentle, 0.15 = strong. Default 0.05. */\n    strength?: number;\n    /** Pixel radius of influence. Default 220. */\n    radius?: number;\n}\n\ninterface BoidsEcosystemProps {\n    count?: number;\n    background?: string;\n    palette?: string[];\n    cursorRadius?: number;\n    /** Force points the flock is drawn toward. Read live, safe to mutate by ref. */\n    attractors?: BoidAttractor[];\n    /**\n     * Receives the live agents array on mount. Mutate-in-place by the simulation;\n     * read positions from animation frames in the parent (do NOT trigger React state from this).\n     */\n    agentsRef?: RefObject<BoidAgent[] | null>;\n    /** Triangle reads as flock; dot reads as ambient activity. */\n    agentShape?: \"triangle\" | \"dot\";\n    className?: string;\n    children?: React.ReactNode;\n}\n\nconst DEFAULT_PALETTE = [\"#f5f5f4\", \"#fde68a\", \"#93c5fd\", \"#fca5a5\"];\n\nexport default function BoidsEcosystem({\n    count = 120,\n    background = \"#0b0b12\",\n    palette = DEFAULT_PALETTE,\n    cursorRadius = 90,\n    attractors,\n    agentsRef,\n    agentShape = \"triangle\",\n    className,\n    children,\n}: BoidsEcosystemProps) {\n    const canvasRef = useRef<HTMLCanvasElement>(null);\n    const cursorRef = useRef<{ x: number; y: number; active: boolean }>({\n        x: 0,\n        y: 0,\n        active: false,\n    });\n    // All non-`count` props read from this ref each frame, so prop changes\n    // don't tear down the simulation.\n    const liveRef = useRef({ background, palette, cursorRadius, attractors, agentShape });\n    liveRef.current = { background, palette, cursorRadius, attractors, agentShape };\n\n    useEffect(() => {\n        const canvas = canvasRef.current;\n        if (!canvas) return;\n        const ctx = canvas.getContext(\"2d\");\n        if (!ctx) return;\n\n        const dpr = Math.min(window.devicePixelRatio || 1, 2);\n        const agents: BoidAgent[] = [];\n\n        const resize = () => {\n            const rect = canvas.getBoundingClientRect();\n            canvas.width = rect.width * dpr;\n            canvas.height = rect.height * dpr;\n            ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n        };\n        resize();\n        const ro = new ResizeObserver(resize);\n        ro.observe(canvas);\n\n        const w = () => canvas.width / dpr;\n        const h = () => canvas.height / dpr;\n\n        const initialPalette = liveRef.current.palette;\n        for (let i = 0; i < count; i++) {\n            const a = Math.random() * Math.PI * 2;\n            const s = 0.6 + Math.random() * 0.6;\n            agents.push({\n                x: Math.random() * w(),\n                y: Math.random() * h(),\n                vx: Math.cos(a) * s,\n                vy: Math.sin(a) * s,\n                color: initialPalette[Math.floor(Math.random() * initialPalette.length)],\n            });\n        }\n        if (agentsRef) agentsRef.current = agents;\n\n        const perception = 36;\n        const maxSpeed = 1.8;\n\n        let raf = 0;\n        const step = () => {\n            const { background: bg, cursorRadius: cr, attractors: atts } = liveRef.current;\n            const W = w();\n            const H = h();\n\n            ctx.fillStyle = `${bg}dd`;\n            ctx.fillRect(0, 0, W, H);\n\n            for (const a of agents) {\n                let ax = 0;\n                let ay = 0;\n                let sx = 0;\n                let sy = 0;\n                let cx = 0;\n                let cy = 0;\n                let n = 0;\n\n                for (const b of agents) {\n                    if (a === b) continue;\n                    const dx = b.x - a.x;\n                    const dy = b.y - a.y;\n                    const d2 = dx * dx + dy * dy;\n                    if (d2 < perception * perception) {\n                        ax += b.vx;\n                        ay += b.vy;\n                        cx += b.x;\n                        cy += b.y;\n                        if (d2 < 18 * 18 && d2 > 0.0001) {\n                            const d = Math.sqrt(d2);\n                            sx -= dx / d;\n                            sy -= dy / d;\n                        }\n                        n++;\n                    }\n                }\n                if (n > 0) {\n                    a.vx += (ax / n - a.vx) * 0.04;\n                    a.vy += (ay / n - a.vy) * 0.04;\n                    a.vx += (cx / n - a.x) * 0.0006;\n                    a.vy += (cy / n - a.y) * 0.0006;\n                    a.vx += sx * 0.06;\n                    a.vy += sy * 0.06;\n                }\n\n                if (atts?.length) {\n                    for (const att of atts) {\n                        const tx = att.x * W;\n                        const ty = att.y * H;\n                        const dx = tx - a.x;\n                        const dy = ty - a.y;\n                        const d = Math.hypot(dx, dy) || 1;\n                        const radius = att.radius ?? 220;\n                        if (d < radius) {\n                            const f = ((radius - d) / radius) * (att.strength ?? 0.05);\n                            a.vx += (dx / d) * f;\n                            a.vy += (dy / d) * f;\n                        }\n                    }\n                }\n\n                if (cursorRef.current.active) {\n                    const dx = a.x - cursorRef.current.x;\n                    const dy = a.y - cursorRef.current.y;\n                    const d = Math.hypot(dx, dy);\n                    if (d < cr && d > 0) {\n                        const f = (cr - d) / cr;\n                        a.vx += (dx / d) * f * 0.6;\n                        a.vy += (dy / d) * f * 0.6;\n                    }\n                }\n\n                const sp = Math.hypot(a.vx, a.vy);\n                if (sp > maxSpeed) {\n                    a.vx = (a.vx / sp) * maxSpeed;\n                    a.vy = (a.vy / sp) * maxSpeed;\n                }\n                a.x += a.vx;\n                a.y += a.vy;\n\n                if (a.x < 0) a.x += W;\n                if (a.x > W) a.x -= W;\n                if (a.y < 0) a.y += H;\n                if (a.y > H) a.y -= H;\n\n                ctx.fillStyle = a.color;\n                ctx.globalAlpha = 0.88;\n                if (liveRef.current.agentShape === \"dot\") {\n                    ctx.beginPath();\n                    ctx.arc(a.x, a.y, 1.8, 0, Math.PI * 2);\n                    ctx.fill();\n                } else {\n                    const angle = Math.atan2(a.vy, a.vx);\n                    ctx.save();\n                    ctx.translate(a.x, a.y);\n                    ctx.rotate(angle);\n                    ctx.beginPath();\n                    ctx.moveTo(6, 0);\n                    ctx.lineTo(-4, 3);\n                    ctx.lineTo(-4, -3);\n                    ctx.closePath();\n                    ctx.fill();\n                    ctx.restore();\n                }\n            }\n            raf = requestAnimationFrame(step);\n        };\n        raf = requestAnimationFrame(step);\n\n        // Listen on window so events bubble through any overlay (links, content\n        // wrappers, etc.). Bounds-check inside the handler so the flock only\n        // reacts when the cursor is within the canvas itself.\n        const onMove = (e: PointerEvent) => {\n            const r = canvas.getBoundingClientRect();\n            const x = e.clientX - r.left;\n            const y = e.clientY - r.top;\n            cursorRef.current.x = x;\n            cursorRef.current.y = y;\n            cursorRef.current.active = x >= 0 && x <= r.width && y >= 0 && y <= r.height;\n        };\n        const onPageLeave = () => {\n            cursorRef.current.active = false;\n        };\n        window.addEventListener(\"pointermove\", onMove);\n        document.documentElement.addEventListener(\"pointerleave\", onPageLeave);\n        window.addEventListener(\"blur\", onPageLeave);\n\n        return () => {\n            cancelAnimationFrame(raf);\n            ro.disconnect();\n            window.removeEventListener(\"pointermove\", onMove);\n            document.documentElement.removeEventListener(\"pointerleave\", onPageLeave);\n            window.removeEventListener(\"blur\", onPageLeave);\n            if (agentsRef) agentsRef.current = null;\n        };\n    }, [count, agentsRef]);\n\n    return (\n        <div className={cn(\"relative h-full w-full overflow-hidden rounded-lg\", className)}>\n            <canvas ref={canvasRef} className=\"absolute inset-0 h-full w-full\" style={{ background }} />\n            {children && (\n                <div className=\"relative z-10 flex h-full items-center justify-center pointer-events-none\">\n                    {children}\n                </div>\n            )}\n        </div>\n    );\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/utilities/cn.ts",
      "content": "import { ClassValue, clsx } from \"clsx\";\r\nimport { twMerge } from \"tailwind-merge\";\r\n\r\nexport function cn(...inputs: ClassValue[]) {\r\n\treturn twMerge(clsx(inputs));\r\n}\r\n",
      "type": "registry:ui"
    }
  ]
}