{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "token-fall",
  "title": "Token Fall",
  "description": "Physics tokens that fall with gravity, bounce and scatter away from the cursor.",
  "dependencies": [
    "matter-js",
    "@types/matter-js"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/clover-ui/token-fall.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport Matter from \"matter-js\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface TokenPalette {\n  bg: string;\n  fg: string;\n}\n\nexport interface TokenMeta {\n  id: number;\n  size: number;\n  palette: TokenPalette;\n  angle: number;\n}\n\nexport interface TokenFallProps {\n  count?: number;\n\n  minSize?: number;\n\n  maxSize?: number;\n\n  palettes?: TokenPalette[];\n\n  renderToken?: (token: TokenMeta) => React.ReactNode;\n\n  gravity?: number;\n\n  restitution?: number;\n\n  repelRadius?: number;\n\n  repelStrength?: number;\n\n  spawnInterval?: number;\n\n  className?: string;\n  tokenClassName?: string;\n}\n\nconst DEFAULT_PALETTES: TokenPalette[] = [\n  { bg: \"#EA6D00\", fg: \"#FFC24B\" },\n  { bg: \"#C82929\", fg: \"#EB8900\" },\n  { bg: \"#1F6F50\", fg: \"#2ECC71\" },\n  { bg: \"#5B3AA0\", fg: \"#9B6EF3\" },\n  { bg: \"#1E5FA8\", fg: \"#4FA8FF\" },\n];\nconst TOKEN_PATH =\n  \"M7.48501 2.26812C7.18501 2.09812 6.81501 2.09812 6.51501 2.26812L3.01501 4.21312L5.55001 5.61812C5.91501 5.23812 6.43001 4.99813 7.00001 4.99813C7.57001 4.99813 8.08501 5.23812 8.45001 5.61812L10.985 4.20813L7.48501 2.26812ZM6.00001 6.99813C6.00001 6.44813 6.45001 5.99813 7.00001 5.99813C7.55001 5.99813 8.00001 6.44813 8.00001 6.99813C8.00001 7.54813 7.55001 7.99813 7.00001 7.99813C6.45001 7.99813 6.00001 7.54813 6.00001 6.99813ZM2.50001 5.06813L5.06501 6.49312C5.02001 6.65312 5.00001 6.82313 5.00001 6.99813C5.00001 7.92813 5.63501 8.71313 6.50001 8.93313V11.7181L3.01501 9.78312C2.85852 9.69694 2.72812 9.57022 2.6375 9.41626C2.54688 9.26229 2.49939 9.08677 2.50001 8.90812V5.06813ZM7.50001 11.7181V8.93313C8.36501 8.71313 9.00001 7.92813 9.00001 6.99813C9.00001 6.82313 8.98001 6.65312 8.93501 6.49312L11.5 5.06813V8.90812C11.5 9.27312 11.305 9.60812 10.985 9.78312L7.50001 11.7181Z\";\n\nfunction TokenGlyph({ size, palette }: { size: number; palette: TokenPalette }) {\n  return (\n    <svg width={size} height={size} viewBox=\"0 0 14 14\" fill=\"none\">\n      <rect width=\"14\" height=\"14\" rx=\"7\" fill={palette.bg} />\n      <path d={TOKEN_PATH} fill={palette.fg} />\n    </svg>\n  );\n}\n\ntype TokenBody = TokenMeta & { body: Matter.Body };\n\nfunction TokenFall({\n  count = 55,\n  minSize = 14,\n  maxSize = 28,\n  palettes = DEFAULT_PALETTES,\n  renderToken,\n  gravity = 0.35,\n  restitution = 0.75,\n  repelRadius = 150,\n  repelStrength = 0.0015,\n  spawnInterval = 180,\n  className,\n  tokenClassName,\n}: TokenFallProps) {\n  const containerRef = React.useRef<HTMLDivElement>(null);\n  const domRefs = React.useRef<Record<number, HTMLDivElement | null>>({});\n  const [tokens, setTokens] = React.useState<TokenBody[]>([]);\n\n  React.useEffect(() => {\n    const container = containerRef.current;\n    if (!container) return;\n\n    const width = container.clientWidth;\n    const height = container.clientHeight;\n\n    const engine = Matter.Engine.create();\n    engine.gravity.y = gravity;\n\n    const floor = Matter.Bodies.rectangle(width / 2, height + 20, width + 80, 40, {\n      isStatic: true,\n    });\n    const leftWall = Matter.Bodies.rectangle(-20, height / 2, 40, height * 2, {\n      isStatic: true,\n    });\n    const rightWall = Matter.Bodies.rectangle(width + 20, height / 2, 40, height * 2, {\n      isStatic: true,\n    });\n\n    Matter.World.add(engine.world, [floor, leftWall, rightWall]);\n\n    const created: TokenBody[] = [];\n\n    const mouse = { x: -1000, y: -1000 };\n\n    const handleMouseMove = (event: MouseEvent) => {\n      const rect = container.getBoundingClientRect();\n      mouse.x = event.clientX - rect.left;\n      mouse.y = event.clientY - rect.top;\n    };\n\n    const handleMouseLeave = () => {\n      mouse.x = -1000;\n      mouse.y = -1000;\n    };\n\n    container.addEventListener(\"mousemove\", handleMouseMove);\n    container.addEventListener(\"mouseleave\", handleMouseLeave);\n\n    let nextId = 0;\n    const spawnToken = () => {\n      if (created.length >= count) return;\n\n      const size = minSize + Math.random() * (maxSize - minSize);\n      const palette = palettes[Math.floor(Math.random() * palettes.length)];\n      const x = Math.random() * width;\n      const y = -size - Math.random() * 80;\n\n      const body = Matter.Bodies.circle(x, y, size / 2, {\n        restitution,\n        friction: 0.05,\n        frictionAir: 0.003,\n        density: 0.0012,\n      });\n      Matter.Body.setVelocity(body, {\n        x: (Math.random() - 0.5) * 2.5,\n        y: Math.random() * 1.5 + 0.5,\n      });\n      Matter.Body.setAngularVelocity(body, (Math.random() - 0.5) * 0.3);\n\n      Matter.World.add(engine.world, body);\n\n      created.push({\n        id: nextId++,\n        body,\n        size,\n        palette,\n        angle: body.angle,\n      });\n\n      setTokens([...created]);\n    };\n    const spawnLoop = window.setInterval(spawnToken, spawnInterval);\n    spawnToken();\n    const beforeUpdate = () => {\n      created.forEach((token) => {\n        const dx = token.body.position.x - mouse.x;\n        const dy = token.body.position.y - mouse.y;\n        const distance = Math.sqrt(dx * dx + dy * dy);\n\n        if (distance > 0 && distance < repelRadius) {\n          const strength = (1 - distance / repelRadius) * repelStrength;\n          Matter.Body.applyForce(token.body, token.body.position, {\n            x: (dx / distance) * strength,\n            y: (dy / distance) * strength,\n          });\n        }\n      });\n    };\n\n    Matter.Events.on(engine, \"beforeUpdate\", beforeUpdate);\n\n    const runner = Matter.Runner.create();\n    Matter.Runner.run(runner, engine);\n\n    let frame: number;\n    const sync = () => {\n      created.forEach((token) => {\n        const el = domRefs.current[token.id];\n        if (!el) return;\n\n        const { x, y } = token.body.position;\n        el.style.transform =\n          `translate3d(${x - token.size / 2}px, ${y - token.size / 2}px, 0) ` +\n          `rotate(${token.body.angle}rad)`;\n      });\n\n      frame = requestAnimationFrame(sync);\n    };\n    frame = requestAnimationFrame(sync);\n\n    return () => {\n      cancelAnimationFrame(frame);\n      window.clearInterval(spawnLoop);\n\n      container.removeEventListener(\"mousemove\", handleMouseMove);\n      container.removeEventListener(\"mouseleave\", handleMouseLeave);\n\n      Matter.Events.off(engine, \"beforeUpdate\", beforeUpdate);\n      Matter.Runner.stop(runner);\n      Matter.World.clear(engine.world, false);\n      Matter.Engine.clear(engine);\n    };\n  }, [\n    count,\n    minSize,\n    maxSize,\n    palettes,\n    gravity,\n    restitution,\n    repelRadius,\n    repelStrength,\n    spawnInterval,\n  ]);\n\n  return (\n    <div\n      ref={containerRef}\n      aria-hidden=\"true\"\n      className={cn(\"pointer-events-none absolute inset-0 overflow-hidden\", className)}\n    >\n      {tokens.map((token) => (\n        <div\n          key={token.id}\n          ref={(el) => {\n            domRefs.current[token.id] = el;\n          }}\n          className={cn(\n            \"pointer-events-auto absolute top-0 left-0 cursor-grab rounded-full will-change-transform active:cursor-grabbing\",\n            tokenClassName,\n          )}\n          style={{ width: token.size, height: token.size }}\n        >\n          {renderToken ? (\n            renderToken(token)\n          ) : (\n            <TokenGlyph size={token.size} palette={token.palette} />\n          )}\n        </div>\n      ))}\n    </div>\n  );\n}\n\nexport { TokenFall };\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}