{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "orbiting-circles",
  "title": "Orbiting Circles",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "Items that orbit a center on circular tracks while staying upright. Evenly spaced, any radius, speed and direction; stack several for rings. Paused off-screen, static under reduced motion.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/orbiting-circles.tsx",
      "content": "// Ballmac UI: Orbiting Circles. https://ui.ballmac.com/components/orbiting-circles\n\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\ntype OrbitingCirclesProps = React.ComponentProps<\"div\"> & {\n  /** Radius of the orbit in pixels. */\n  radius?: number\n  /** Seconds for one full orbit. */\n  duration?: number\n  /** Orbit counter-clockwise. */\n  reverse?: boolean\n  /** Draw the circular track the items travel on. */\n  path?: boolean\n  /** Width and height of each orbiting item in pixels. */\n  iconSize?: number\n  /** Angle in degrees where the first item starts; the rest are spaced evenly. */\n  startAngle?: number\n}\n\nfunction OrbitingCircles({\n  radius = 120,\n  duration = 24,\n  reverse = false,\n  path = true,\n  iconSize = 40,\n  startAngle = -90,\n  className,\n  children,\n  ...props\n}: OrbitingCirclesProps) {\n  const rootRef = React.useRef<HTMLDivElement>(null)\n  const items = React.Children.toArray(children)\n  const count = items.length\n\n  React.useEffect(() => {\n    const root = rootRef.current\n    if (!root || typeof root.animate !== \"function\") return\n    if (window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) return\n    const turn = reverse ? -360 : 360\n    const animations = Array.from(root.querySelectorAll<HTMLElement>(\"[data-slot=orbiting-circles-item]\")).map(\n      (node, i) => {\n        const angle = startAngle + (360 / Math.max(count, 1)) * i\n        // Rotate out, translate to the rim, rotate back: the item orbits but stays upright.\n        return node.animate(\n          [\n            { transform: `rotate(${angle}deg) translateX(${radius}px) rotate(${-angle}deg)` },\n            { transform: `rotate(${angle + turn}deg) translateX(${radius}px) rotate(${-angle - turn}deg)` },\n          ],\n          { duration: duration * 1000, iterations: Infinity, easing: \"linear\" }\n        )\n      }\n    )\n    let visible = true\n    const sync = () => {\n      for (const a of animations) {\n        if (visible && !document.hidden) a.play()\n        else a.pause()\n      }\n    }\n    const io = new IntersectionObserver(([entry]) => {\n      visible = entry?.isIntersecting ?? true\n      sync()\n    })\n    io.observe(root)\n    document.addEventListener(\"visibilitychange\", sync)\n    return () => {\n      io.disconnect()\n      document.removeEventListener(\"visibilitychange\", sync)\n      for (const a of animations) a.cancel()\n    }\n  }, [radius, duration, reverse, startAngle, count])\n\n  return (\n    <div\n      ref={rootRef}\n      data-slot=\"orbiting-circles\"\n      className={cn(\"pointer-events-none absolute inset-0 flex items-center justify-center\", className)}\n      {...props}\n    >\n      {path && (\n        <svg\n          aria-hidden=\"true\"\n          data-slot=\"orbiting-circles-path\"\n          className=\"absolute overflow-visible\"\n          width={radius * 2}\n          height={radius * 2}\n        >\n          <circle\n            cx={radius}\n            cy={radius}\n            r={radius}\n            fill=\"none\"\n            strokeWidth={1}\n            className=\"stroke-foreground/10\"\n            strokeDasharray=\"2 5\"\n          />\n        </svg>\n      )}\n      {items.map((child, i) => {\n        const angle = startAngle + (360 / Math.max(count, 1)) * i\n        return (\n          <div\n            key={i}\n            data-slot=\"orbiting-circles-item\"\n            className=\"pointer-events-auto absolute flex items-center justify-center\"\n            style={{\n              width: iconSize,\n              height: iconSize,\n              // Static position for the server render and reduced motion; the animation takes over after mount.\n              transform: `rotate(${angle}deg) translateX(${radius}px) rotate(${-angle}deg)`,\n            }}\n          >\n            {child}\n          </div>\n        )\n      })}\n    </div>\n  )\n}\n\nexport { OrbitingCircles, type OrbitingCirclesProps }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/orbiting-circles.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "orbit",
      "integrations",
      "icons",
      "rings",
      "hero",
      "motion"
    ],
    "version": "1.0.0",
    "updated": "2026-09-29",
    "ai": {
      "summary": "Inside a relative container with a fixed height, render the center element, then one <OrbitingCircles radius duration> per ring with the orbiting items as children. Each ring fills the container (absolute inset-0) and centers itself; items are laid out evenly around the circle.",
      "whenToUse": [
        "An integrations or ecosystem section: your product in the middle, tools around it",
        "A hero visual for a platform that connects many services"
      ],
      "whenNotToUse": [
        "Items people must read or click reliably (moving targets); use a grid",
        "More than about 8 items per ring"
      ],
      "composesWith": [
        "animated-beam",
        "dot-pattern"
      ],
      "a11y": [],
      "customization": [
        "radius (px), duration (s per orbit), reverse, startAngle (degrees)",
        "iconSize: the box each item gets (px)",
        "path toggles the dashed track; restyle it with [&_[data-slot=orbiting-circles-path]_circle]:stroke-…"
      ]
    },
    "examples": [
      "orbiting-circles-demo",
      "orbiting-circles-minimal"
    ],
    "url": "https://ui.ballmac.com/components/orbiting-circles"
  },
  "categories": [
    "motion"
  ],
  "type": "registry:ui"
}