{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "SwarmCursor-JS-CSS",
	"title": "SwarmCursor",
	"description": "Flocking particle swarm that chases the pointer, jostles for space and drifts apart at rest.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:file",
			"path": "SwarmCursor.css",
			"target": "@components/SwarmCursor.css",
			"content": ".swarm-cursor {\n  position: relative;\n  width: 100%;\n  height: 100%;\n}\n\n.swarm-cursor__canvas {\n  position: absolute;\n  inset: 0;\n  width: 100%;\n  height: 100%;\n  display: block;\n  pointer-events: none;\n  user-select: none;\n}\n\n.swarm-cursor__content {\n  position: absolute;\n  inset: 0;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  pointer-events: none;\n}\n"
		},
		{
			"type": "registry:component",
			"path": "SwarmCursor.jsx",
			"content": "import { useEffect, useRef } from 'react';\nimport { Renderer, Program, Mesh, Geometry, Triangle, RenderTarget } from 'ogl';\n\nimport './SwarmCursor.css';\n\nconst FIELD_VERT = `\nprecision highp float;\nattribute vec2 position;\nattribute vec2 aLocal;\nattribute float aWeight;\nuniform vec2 uRes;\nvarying vec2 vLocal;\nvarying float vWeight;\n\nvoid main() {\n  vLocal = aLocal;\n  vWeight = aWeight;\n  vec2 clip = (position / uRes) * 2.0 - 1.0;\n  gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);\n}\n`;\n\nconst FIELD_FRAG = `\nprecision highp float;\nvarying vec2 vLocal;\nvarying float vWeight;\n\nvoid main() {\n  float d = length(vLocal);\n  float a = exp(-d * d * 3.6) * vWeight;\n  gl_FragColor = vec4(a, a, a, a);\n}\n`;\n\nconst SCREEN_VERT = `\nprecision highp float;\nattribute vec2 uv;\nattribute vec2 position;\nvarying vec2 vUv;\nvoid main() {\n  vUv = uv;\n  gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst COMP_FRAG = `\nprecision highp float;\nuniform sampler2D tField;\nuniform vec3 uColor;\nuniform vec3 uAccent;\nuniform float uMerge;\nuniform float uGlow;\nuniform float uOpacity;\nvarying vec2 vUv;\n\nvoid main() {\n  float f = texture2D(tField, vUv).r;\n\n  float edge = uMerge * 0.3;\n  float core = smoothstep(uMerge - edge, uMerge + edge, f);\n  float halo = smoothstep(uMerge * 0.12, uMerge, f);\n\n  vec3 col = mix(uColor, uAccent, clamp(f / max(uMerge * 2.4, 0.001), 0.0, 1.0));\n\n  float alpha = (core + halo * uGlow * (1.0 - core)) * uOpacity;\n  if (alpha <= 0.002) discard;\n  gl_FragColor = vec4(col, clamp(alpha, 0.0, 1.0));\n}\n`;\n\nconst hexToRgb = hex => {\n  let h = (hex || '').replace('#', '').trim();\n  if (h.length === 3)\n    h = h\n      .split('')\n      .map(c => c + c)\n      .join('');\n  const n = parseInt(h || '000000', 16);\n  return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];\n};\n\nconst buildPerm = () => {\n  const src = new Uint8Array(256);\n  for (let i = 0; i < 256; i++) src[i] = i;\n  for (let i = 255; i > 0; i--) {\n    const j = (Math.random() * (i + 1)) | 0;\n    const t = src[i];\n    src[i] = src[j];\n    src[j] = t;\n  }\n  const perm = new Uint16Array(512);\n  for (let i = 0; i < 512; i++) perm[i] = src[i & 255];\n  return perm;\n};\n\nconst smoothFade = t => t * t * t * (t * (t * 6 - 15) + 10);\n\nconst gradDot = (h, x, y, z) => {\n  const u = h < 8 ? x : y;\n  const v = h < 4 ? y : h === 12 || h === 14 ? x : z;\n  return ((h & 1) === 0 ? u : -u) + ((h & 2) === 0 ? v : -v);\n};\n\nconst noise3 = (perm, x, y, z) => {\n  const fx = Math.floor(x);\n  const fy = Math.floor(y);\n  const fz = Math.floor(z);\n  const X = fx & 255;\n  const Y = fy & 255;\n  const Z = fz & 255;\n  const rx = x - fx;\n  const ry = y - fy;\n  const rz = z - fz;\n  const u = smoothFade(rx);\n  const v = smoothFade(ry);\n  const w = smoothFade(rz);\n\n  const A = perm[X] + Y;\n  const AA = perm[A & 511] + Z;\n  const AB = perm[(A + 1) & 511] + Z;\n  const B = perm[(X + 1) & 511] + Y;\n  const BA = perm[B & 511] + Z;\n  const BB = perm[(B + 1) & 511] + Z;\n\n  const g000 = gradDot(perm[AA & 511] & 15, rx, ry, rz);\n  const g100 = gradDot(perm[BA & 511] & 15, rx - 1, ry, rz);\n  const g010 = gradDot(perm[AB & 511] & 15, rx, ry - 1, rz);\n  const g110 = gradDot(perm[BB & 511] & 15, rx - 1, ry - 1, rz);\n  const g001 = gradDot(perm[(AA + 1) & 511] & 15, rx, ry, rz - 1);\n  const g101 = gradDot(perm[(BA + 1) & 511] & 15, rx - 1, ry, rz - 1);\n  const g011 = gradDot(perm[(AB + 1) & 511] & 15, rx, ry - 1, rz - 1);\n  const g111 = gradDot(perm[(BB + 1) & 511] & 15, rx - 1, ry - 1, rz - 1);\n\n  const x00 = g000 + u * (g100 - g000);\n  const x10 = g010 + u * (g110 - g010);\n  const x01 = g001 + u * (g101 - g001);\n  const x11 = g011 + u * (g111 - g011);\n  const y0 = x00 + v * (x10 - x00);\n  const y1 = x01 + v * (x11 - x01);\n  return y0 + w * (y1 - y0);\n};\n\nconst SwarmCursor = ({\n  color = '#ffffff',\n  accentColor = '#ffffff',\n  count = 10,\n  size = 10,\n  merge = 0.77,\n  glow = 0.75,\n  opacity = 1,\n  spread = 100,\n  separation = 0.15,\n  speed = 2.5,\n  wander = 0.25,\n  trail = 0.75,\n  scatterOnClick = true,\n  enabled = true,\n  children,\n  className = '',\n  style,\n  ...rest\n}) => {\n  const containerRef = useRef(null);\n  const propsRef = useRef({});\n  propsRef.current = {\n    color,\n    accentColor,\n    count,\n    size,\n    merge,\n    glow,\n    opacity,\n    spread,\n    separation,\n    speed,\n    wander,\n    trail,\n    scatterOnClick,\n    enabled\n  };\n\n  useEffect(() => {\n    const container = containerRef.current;\n    if (!container) return;\n\n    const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\n    const renderer = new Renderer({ alpha: true, dpr: Math.min(window.devicePixelRatio || 1, 1.75) });\n    const gl = renderer.gl;\n    gl.clearColor(0, 0, 0, 0);\n    gl.canvas.className = 'swarm-cursor__canvas';\n    container.appendChild(gl.canvas);\n\n    const MAX = 120;\n    const MAX_QUADS = 6000;\n    const HISTORY = 120;\n    const positions = new Float32Array(MAX_QUADS * 4 * 2);\n    const locals = new Float32Array(MAX_QUADS * 4 * 2);\n    const weights = new Float32Array(MAX_QUADS * 4);\n    const index = new Uint16Array(MAX_QUADS * 6);\n    for (let i = 0; i < MAX_QUADS; i++) {\n      const v = i * 4;\n      locals.set([-1, -1, 1, -1, 1, 1, -1, 1], v * 2);\n      index.set([v, v + 1, v + 2, v, v + 2, v + 3], i * 6);\n    }\n\n    const geometry = new Geometry(gl, {\n      position: { size: 2, data: positions, usage: gl.DYNAMIC_DRAW },\n      aLocal: { size: 2, data: locals },\n      aWeight: { size: 1, data: weights, usage: gl.DYNAMIC_DRAW },\n      index: { data: index }\n    });\n\n    const fieldProgram = new Program(gl, {\n      vertex: FIELD_VERT,\n      fragment: FIELD_FRAG,\n      uniforms: { uRes: { value: [1, 1] } },\n      transparent: true,\n      depthTest: false,\n      depthWrite: false,\n      cullFace: false\n    });\n    fieldProgram.setBlendFunc(gl.ONE, gl.ONE);\n    const fieldMesh = new Mesh(gl, { geometry, program: fieldProgram });\n\n    const compProgram = new Program(gl, {\n      vertex: SCREEN_VERT,\n      fragment: COMP_FRAG,\n      uniforms: {\n        tField: { value: null },\n        uColor: { value: hexToRgb(propsRef.current.color) },\n        uAccent: { value: hexToRgb(propsRef.current.accentColor) },\n        uMerge: { value: propsRef.current.merge },\n        uGlow: { value: propsRef.current.glow },\n        uOpacity: { value: propsRef.current.opacity }\n      },\n      transparent: true,\n      depthTest: false,\n      depthWrite: false,\n      cullFace: false\n    });\n    const compMesh = new Mesh(gl, { geometry: new Triangle(gl), program: compProgram });\n\n    let target = null;\n    let cssW = 1;\n    let cssH = 1;\n\n    const resize = () => {\n      cssW = container.clientWidth || 1;\n      cssH = container.clientHeight || 1;\n      renderer.setSize(cssW, cssH);\n      fieldProgram.uniforms.uRes.value = [cssW, cssH];\n      const w = Math.max(1, Math.round(gl.drawingBufferWidth));\n      const h = Math.max(1, Math.round(gl.drawingBufferHeight));\n      target = new RenderTarget(gl, { width: w, height: h, depth: false });\n    };\n    const ro = new ResizeObserver(resize);\n    ro.observe(container);\n    resize();\n\n    const perm = buildPerm();\n    const px = new Float32Array(MAX);\n    const py = new Float32Array(MAX);\n    const vx = new Float32Array(MAX);\n    const vy = new Float32Array(MAX);\n    const scale = new Float32Array(MAX);\n    const agility = new Float32Array(MAX);\n    const handed = new Float32Array(MAX);\n    const noiseX = new Float32Array(MAX);\n    const noiseY = new Float32Array(MAX);\n\n    const histX = new Float32Array(HISTORY * MAX);\n    const histY = new Float32Array(HISTORY * MAX);\n    const histT = new Float32Array(HISTORY);\n    let histHead = 0;\n    let histLen = 0;\n    let lastSample = -1;\n\n    const spawn = (i, ox, oy) => {\n      const a = Math.random() * Math.PI * 2;\n      const r = 40 + Math.random() * 120;\n      px[i] = ox + Math.cos(a) * r;\n      py[i] = oy + Math.sin(a) * r;\n      vx[i] = Math.cos(a) * 60;\n      vy[i] = Math.sin(a) * 60;\n      for (let h = 0; h < HISTORY; h++) {\n        histX[h * MAX + i] = px[i];\n        histY[h * MAX + i] = py[i];\n      }\n    };\n\n    for (let i = 0; i < MAX; i++) {\n      spawn(i, cssW * 0.5, cssH * 0.5);\n      scale[i] = 0.65 + Math.random() * 0.6;\n      agility[i] = 0.75 + Math.random() * 0.5;\n      handed[i] = Math.random() < 0.5 ? -1 : 1;\n      noiseX[i] = Math.random() * 260;\n      noiseY[i] = Math.random() * 260;\n    }\n\n    const cursor = { x: cssW * 0.5, y: cssH * 0.5, has: false };\n    let burst = 0;\n    let activeCount = Math.max(1, Math.min(MAX, Math.round(propsRef.current.count)));\n\n    const onMove = e => {\n      const r = container.getBoundingClientRect();\n      cursor.x = e.clientX - r.left;\n      cursor.y = e.clientY - r.top;\n      cursor.has = true;\n    };\n    const onLeave = () => {\n      cursor.has = false;\n    };\n    const onDown = e => {\n      if (!propsRef.current.scatterOnClick || !propsRef.current.enabled) return;\n      const r = container.getBoundingClientRect();\n      const cx = e.clientX - r.left;\n      const cy = e.clientY - r.top;\n      const escape = 620 + propsRef.current.speed * 130;\n      for (let i = 0; i < MAX; i++) {\n        let dx = px[i] - cx;\n        let dy = py[i] - cy;\n        let d = Math.hypot(dx, dy);\n        if (d < 1e-3) {\n          const a = Math.random() * Math.PI * 2;\n          dx = Math.cos(a);\n          dy = Math.sin(a);\n          d = 1;\n        }\n        const kick = escape * (0.75 + Math.random() * 0.5);\n        vx[i] = (dx / d) * kick;\n        vy[i] = (dy / d) * kick;\n      }\n      burst = 1;\n    };\n    container.addEventListener('pointermove', onMove, { passive: true });\n    container.addEventListener('pointerenter', onMove, { passive: true });\n    container.addEventListener('pointerleave', onLeave);\n    container.addEventListener('pointerdown', onDown);\n\n    let raf = 0;\n    let last = performance.now();\n\n    const frame = now => {\n      raf = requestAnimationFrame(frame);\n      const p = propsRef.current;\n      const dt = Math.min((now - last) / 1000, 0.05);\n      last = now;\n\n      if (!p.enabled || reduceMotion) {\n        renderer.render({ scene: compMesh });\n        return;\n      }\n\n      const n = Math.max(1, Math.min(MAX, Math.round(p.count)));\n      const anchorX = cursor.has ? cursor.x : cssW * 0.5;\n      const anchorY = cursor.has ? cursor.y : cssH * 0.5;\n\n      for (let i = activeCount; i < n; i++) spawn(i, anchorX, anchorY);\n      activeCount = n;\n\n      const t = now * 0.001;\n      burst = Math.max(0, burst - dt / 0.5);\n\n      const maxSpeed = 110 + Math.max(0.1, p.speed) * 165;\n      const steerRate = 4.5 + Math.max(0.1, p.speed) * 1.15;\n      const maxForce = maxSpeed * 9;\n      const band = Math.max(20, p.spread * 0.55);\n      const sepDist = Math.max(1, p.spread * 0.42 * (0.35 + p.separation));\n      const flowMix = p.wander * 2.4;\n      const eps = 0.08;\n      const baseScale = 0.0016;\n      const fineScale = baseScale * 3.6;\n\n      for (let i = 0; i < n; i++) {\n        const dx = anchorX - px[i];\n        const dy = anchorY - py[i];\n        const dist = Math.hypot(dx, dy) || 1e-4;\n        const ux = dx / dist;\n        const uy = dy / dist;\n\n        const orbitDrift = noise3(perm, noiseX[i], noiseY[i], t * 0.13);\n        const orbit = band * (0.34 + 1.35 * Math.max(0, Math.min(1, orbitDrift + 0.5)));\n\n        const radial = Math.max(-1, Math.min(1, (dist - orbit) / (band * 0.85)));\n        const swirl = Math.sqrt(Math.max(0, 1 - radial * radial)) * handed[i];\n\n        let wishX = ux * radial - uy * swirl;\n        let wishY = uy * radial + ux * swirl;\n\n        if (flowMix > 0.001) {\n          const bx = px[i] * baseScale;\n          const by = py[i] * baseScale;\n          const bt = t * 0.22;\n          const coarseX = (noise3(perm, bx, by + eps, bt) - noise3(perm, bx, by - eps, bt)) / (2 * eps);\n          const coarseY = -(noise3(perm, bx + eps, by, bt) - noise3(perm, bx - eps, by, bt)) / (2 * eps);\n\n          const fx = px[i] * fineScale + noiseX[i];\n          const fy = py[i] * fineScale + noiseY[i];\n          const ft = t * 0.55;\n          const fineX = (noise3(perm, fx, fy + eps, ft) - noise3(perm, fx, fy - eps, ft)) / (2 * eps);\n          const fineY = -(noise3(perm, fx + eps, fy, ft) - noise3(perm, fx - eps, fy, ft)) / (2 * eps);\n\n          wishX += (coarseX + fineX * 0.7) * flowMix;\n          wishY += (coarseY + fineY * 0.7) * flowMix;\n        }\n\n        const wl = Math.hypot(wishX, wishY) || 1e-4;\n        wishX /= wl;\n        wishY /= wl;\n\n        const rate = steerRate * agility[i] * (1 - burst);\n        let ax = (wishX * maxSpeed - vx[i]) * rate;\n        let ay = (wishY * maxSpeed - vy[i]) * rate;\n\n        if (burst > 0.001) {\n          ax -= ux * maxSpeed * burst * 5.5;\n          ay -= uy * maxSpeed * burst * 5.5;\n        }\n\n        for (let j = 0; j < n; j++) {\n          if (j === i) continue;\n          const sx = px[i] - px[j];\n          const sy = py[i] - py[j];\n          const d2 = sx * sx + sy * sy;\n          if (d2 > 1e-4 && d2 < sepDist * sepDist) {\n            const d = Math.sqrt(d2);\n            const f = (1 - d / sepDist) * maxSpeed * 3.2 * p.separation;\n            ax += (sx / d) * f;\n            ay += (sy / d) * f;\n          }\n        }\n\n        const al = Math.hypot(ax, ay);\n        const cap = maxForce * (1 + burst * 4);\n        if (al > cap) {\n          ax = (ax / al) * cap;\n          ay = (ay / al) * cap;\n        }\n\n        vx[i] += ax * dt;\n        vy[i] += ay * dt;\n\n        const sp = Math.hypot(vx[i], vy[i]);\n        const hi = maxSpeed * (1 + burst * 3.5);\n        const lo = maxSpeed * 0.32;\n        if (sp > hi) {\n          vx[i] = (vx[i] / sp) * hi;\n          vy[i] = (vy[i] / sp) * hi;\n        } else if (sp < lo && sp > 1e-4) {\n          vx[i] = (vx[i] / sp) * lo;\n          vy[i] = (vy[i] / sp) * lo;\n        }\n\n        px[i] += vx[i] * dt;\n        py[i] += vy[i] * dt;\n      }\n\n      const nowSec = now * 0.001;\n      if (lastSample < 0 || nowSec - lastSample >= 0.008) {\n        lastSample = nowSec;\n        histT[histHead] = nowSec;\n        const base = histHead * MAX;\n        for (let i = 0; i < n; i++) {\n          histX[base + i] = px[i];\n          histY[base + i] = py[i];\n        }\n        histHead = (histHead + 1) % HISTORY;\n        if (histLen < HISTORY) histLen++;\n      }\n\n      const trailAge = p.trail * 0.85;\n      const perAgent = Math.max(0, Math.floor(MAX_QUADS / n) - 1);\n      const maxStamps = Math.min(46, perAgent);\n\n      let quad = 0;\n      const pushQuad = (cx, cy, r, w) => {\n        const v = quad * 8;\n        positions[v] = cx - r;\n        positions[v + 1] = cy - r;\n        positions[v + 2] = cx + r;\n        positions[v + 3] = cy - r;\n        positions[v + 4] = cx + r;\n        positions[v + 5] = cy + r;\n        positions[v + 6] = cx - r;\n        positions[v + 7] = cy + r;\n        const o = quad * 4;\n        weights[o] = w;\n        weights[o + 1] = w;\n        weights[o + 2] = w;\n        weights[o + 3] = w;\n        quad++;\n      };\n\n      for (let i = 0; i < n; i++) {\n        const headR = p.size * scale[i] * 2.1;\n        const headW = 1.06 + 0.3 * scale[i];\n        pushQuad(px[i], py[i], headR, headW);\n\n        if (trailAge < 0.01 || maxStamps < 2 || histLen < 2) continue;\n\n        const step = Math.max(2, p.size * scale[i] * 0.5);\n        const span = step * maxStamps;\n\n        let prevX = px[i];\n        let prevY = py[i];\n        let walked = 0;\n        let nextAt = step;\n        let stamps = 0;\n\n        for (let j = 0; j < histLen && stamps < maxStamps; j++) {\n          const slot = (histHead - 1 - j + HISTORY) % HISTORY;\n          if (nowSec - histT[slot] > trailAge) break;\n          const hx = histX[slot * MAX + i];\n          const hy = histY[slot * MAX + i];\n          const segX = hx - prevX;\n          const segY = hy - prevY;\n          const segLen = Math.hypot(segX, segY);\n          if (segLen < 1e-4) continue;\n\n          while (nextAt <= walked + segLen && stamps < maxStamps) {\n            const f = (nextAt - walked) / segLen;\n            const u = nextAt / span;\n            const taper = Math.pow(Math.max(0, 1 - u), 0.55);\n            const rLocal = headR * taper;\n            if (rLocal < step) {\n              stamps = maxStamps;\n              break;\n            }\n            const stampW = Math.min(headW, (headW * step) / (rLocal * 0.934));\n            pushQuad(prevX + segX * f, prevY + segY * f, rLocal, stampW);\n            stamps++;\n            nextAt += step;\n          }\n\n          walked += segLen;\n          prevX = hx;\n          prevY = hy;\n        }\n      }\n\n      geometry.attributes.position.needsUpdate = true;\n      geometry.attributes.aWeight.needsUpdate = true;\n      geometry.setDrawRange(0, quad * 6);\n\n      compProgram.uniforms.uColor.value = hexToRgb(p.color);\n      compProgram.uniforms.uAccent.value = hexToRgb(p.accentColor);\n      compProgram.uniforms.uMerge.value = p.merge;\n      compProgram.uniforms.uGlow.value = p.glow;\n      compProgram.uniforms.uOpacity.value = p.opacity;\n\n      renderer.render({ scene: fieldMesh, target, clear: true });\n      compProgram.uniforms.tField.value = target.texture;\n      renderer.render({ scene: compMesh });\n    };\n    raf = requestAnimationFrame(frame);\n\n    return () => {\n      cancelAnimationFrame(raf);\n      ro.disconnect();\n      container.removeEventListener('pointermove', onMove);\n      container.removeEventListener('pointerenter', onMove);\n      container.removeEventListener('pointerleave', onLeave);\n      container.removeEventListener('pointerdown', onDown);\n      if (gl.canvas.parentElement === container) container.removeChild(gl.canvas);\n      const lose = gl.getExtension('WEBGL_lose_context');\n      if (lose) lose.loseContext();\n    };\n  }, []);\n\n  return (\n    <div ref={containerRef} className={`swarm-cursor ${className}`.trim()} style={style} {...rest}>\n      {children ? <div className=\"swarm-cursor__content\">{children}</div> : null}\n    </div>\n  );\n};\n\nexport default SwarmCursor;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"ogl@^1.0.11"
	]
}