# Orbiting Elements

The OrbitingElements component places children evenly around a circle and rotates them around a centre point, keeping each one upright.

- **Category:** Background
- **Docs:** https://uibeats.com/docs/background/orbiting-elements
- **Registry:** https://uibeats.com/r/orbiting-elements.json
- **License:** MIT
- **npm dependencies:** `lucide-react`, `motion`

## Installation

```bash
npx shadcn@latest add https://uibeats.com/r/orbiting-elements.json
```

Or install the dependencies and copy the source below:

```bash
npm install lucide-react motion
```

## When to use

For showing that several things revolve around one: integrations around a product, tools around a workflow. It reads best with a fixed centrepiece inside the orbit. Two rings at different radii and speeds give you depth, and three or four items per ring keeps them from colliding.

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `React.ReactNode` | `-` | Elements to place around the orbit (required) |
| `radius` | `number` | `100` | Orbit radius in pixels |
| `duration` | `number` | `20` | Seconds for one full revolution |
| `reverse` | `boolean` | `false` | Orbit anticlockwise |
| `showPath` | `boolean` | `true` | Draw the dashed orbit path |
| `startAngle` | `number` | `0` | Degrees to offset the first item by |
| `className` | `string` | `''` | Additional CSS classes for the container |

## Example usage

```tsx
"use client";
import { OrbitingElements } from "@/components/demo/background/orbiting-elements";
import { Atom, Boxes, Cpu, Sparkles } from "lucide-react";

const OrbitingElementsUsage = () => {
  return (
    <div className="relative flex size-full items-center justify-center">
      <div className="absolute flex size-16 items-center justify-center rounded-2xl border bg-card shadow-subtle">
        <Sparkles className="size-6 text-brand" />
      </div>
      <OrbitingElements radius={110} duration={18}>
        {[Atom, Cpu, Boxes].map((Icon, index) => (
          <div
            key={index}
            className="flex size-10 items-center justify-center rounded-full border bg-background shadow-subtle"
          >
            <Icon className="size-4 text-muted-foreground" />
          </div>
        ))}
      </OrbitingElements>
    </div>
  );
};

export default OrbitingElementsUsage;
```

## Source

Save as `components/ui/orbiting-elements.tsx`.

```tsx
"use client";

import { motion, useReducedMotion } from "motion/react";
import { Children, type ReactNode } from "react";

interface OrbitingElementsProps {
  children: ReactNode;
  /** Orbit radius in pixels. */
  radius?: number;
  /** Seconds for one full revolution. */
  duration?: number;
  reverse?: boolean;
  /** Draw the orbit path itself. */
  showPath?: boolean;
  /** Degrees to offset the first item by. */
  startAngle?: number;
  className?: string;
}

/**
 * Places children evenly around a circle and rotates them around a centre
 * point.
 *
 * Each item sits on a rotating arm and counter-rotates at the same speed, so
 * it travels the orbit while staying upright; icons and text never end up
 * upside down at the bottom of the circle.
 */
export function OrbitingElements({
  children,
  radius = 100,
  duration = 20,
  reverse = false,
  showPath = true,
  startAngle = 0,
  className = "",
}: OrbitingElementsProps) {
  const items = Children.toArray(children);
  const prefersReducedMotion = useReducedMotion();
  const spin = reverse ? -360 : 360;

  const transition = {
    duration,
    repeat: Infinity,
    ease: "linear" as const,
  };

  return (
    <div
      className={`relative ${className}`}
      style={{ width: radius * 2, height: radius * 2 }}
    >
      {showPath ? (
        <div
          aria-hidden="true"
          className="absolute inset-0 rounded-full border border-dashed border-border"
        />
      ) : null}

      {items.map((item, index) => {
        const angle = startAngle + (360 / items.length) * index;

        return (
          <motion.div
            key={index}
            className="absolute top-1/2 left-1/2 size-0"
            // The arm: rotates around the centre, carrying the item at
            // `radius` along its length.
            initial={{ rotate: angle }}
            animate={
              prefersReducedMotion ? undefined : { rotate: angle + spin }
            }
            transition={transition}
          >
            {/* `w-max` on both wrappers matters: the arm above is `size-0` so
                the orbit pivots on a point, and without it these inherit a
                zero-width containing block and squash the item into a sliver. */}
            <div
              className="w-max"
              style={{ transform: `translateX(${radius}px)` }}
            >
              <motion.div
                // Counter-rotation, same duration and easing, so the item
                // holds its upright orientation for the whole orbit.
                initial={{ rotate: -angle }}
                animate={
                  prefersReducedMotion ? undefined : { rotate: -angle - spin }
                }
                transition={transition}
                className="flex w-max -translate-x-1/2 -translate-y-1/2 items-center justify-center"
              >
                {item}
              </motion.div>
            </div>
          </motion.div>
        );
      })}
    </div>
  );
}

export default OrbitingElements;
```

## Credits

Written by [Claude Code](https://claude.com/claude-code).
