Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/apursley2012/dev.void/llms.txt

Use this file to discover all available pages before exploring further.

The OrbitingSatellites component turns your blog post links into live satellites — each post orbits a glowing “DATA CORE” node at the center of the section, rotating continuously on its own ring at a unique speed and radius. Hovering over the orbital area pauses all motion, letting visitors read the post tooltip. Clicking any satellite opens a modal that displays the full post excerpt and a “Download Full Transmission” call to action. It’s a blog index unlike any conventional list.
All three orbital rings animate simultaneously using Framer Motion’s animate: { rotate: 360 } with repeat: Infinity. A shared pause state (l) is toggled by mouse enter/leave on the orbital container, which is used to set transition: { duration: 0 } — effectively freezing the animation in place.

Post Data Shape

All posts are stored in a static array (n in the compiled source). Each entry must conform to this object shape:
{
  id: number,           // Unique numeric identifier
  title: string,        // Post title shown in the tooltip and modal
  excerpt: string,      // One-sentence teaser shown in the modal body
  date: string,         // Stardate string, e.g. "Stardate 4592.1"
  orbitRadius: number,  // Orbital ring radius in pixels (half the ring diameter)
  duration: number      // Time in seconds for one full revolution
}

Pre-configured Posts

The component ships with three posts at staggered orbital radii:
TitleDateOrbit RadiusRevolution
The Architecture of NothingnessStardate 4592.1150px20s
Quantum State ManagementStardate 4610.4250px35s
CSS in the Dark ForestStardate 4688.9350px50s
The duration values scale proportionally with orbitRadius — outer orbits take longer to complete, mimicking Kepler’s third law of planetary motion. This makes the animation feel physically grounded rather than arbitrary. When adding new posts, keep this proportionality: roughly duration ≈ orbitRadius / 7 gives a realistic feel.

Layout Architecture

The entire component is a section with min-h-[80vh] flex items-center justify-center relative overflow-hidden. Three types of elements are layered absolutely at the centre:

DATA CORE Node

A w-32 h-32 rounded-full circle with bg-space-900 border-2 border-aurora-teal/30 and a shadow-[0_0_50px_rgba(13,148,136,0.2)] glow. Fixed at the exact centre using absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2. Contains two lines of monospace text: “DATA” and “CORE” in text-aurora-teal.This is the static anchor point that all orbital rings encircle.

Orbital Ring + Satellite

For each post, two elements are centered with the same -translate-x-1/2 -translate-y-1/2 trick:
  1. The ring — a rounded-full border border-slate-800/50 div sized to orbitRadius * 2 × orbitRadius * 2 pixels. Static, purely decorative.
  2. The rotating wrapper — a motion.div with animate: { rotate: 360 } and transition: { duration, repeat: Infinity, ease: "linear" }. This div is absolute inset-0, so it fills the ring exactly. The satellite icon sits at absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 — the 12 o’clock position — and rotates around the ring as the wrapper spins.

Modal Overlay

Conditionally rendered when a post is selected (a !== null). An absolute inset-0 z-50 backdrop with bg-space-950/80 backdrop-blur-sm, containing a motion.div card that scales in from 0.9 to 1.

Orbital Rotation Mechanics

The key to the spinning effect is that Framer Motion rotates the entire invisible inset-0 wrapper div, not the satellite icon itself. Because the icon is pinned to the top-centre of this wrapper, it travels in a circle as the wrapper rotates:
// The rotating wrapper
<motion.div
  className="absolute inset-0 pointer-events-auto"
  animate={{ rotate: 360 }}
  transition={{
    duration: post.duration,   // e.g. 20, 35, or 50 seconds
    repeat: Infinity,
    ease: "linear",
    // Pause condition: duration: 0 freezes the animation
    ...(isPaused || selectedId !== null ? { duration: 0 } : {})
  }}
  onMouseEnter={() => setPaused(true)}
  onMouseLeave={() => setPaused(false)}
>
  {/* Icon at 12 o'clock — moves in a circle as the wrapper rotates */}
  <div className="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2">
    {/* satellite icon + tooltip */}
  </div>
</motion.div>
Setting transition: { duration: 0 } does not snap the satellite to a specific position — it freezes it exactly where it currently is in the rotation. This is the intended behaviour. If you were to use animate: { rotate: 0 } to pause instead, all satellites would snap back to 12 o’clock, which would be jarring.

Pause-on-Hover Behaviour

A single boolean state l (mapped to isPaused in context) controls whether all orbits are paused. The onMouseEnter and onMouseLeave handlers are placed on each rotating motion.div. When any satellite is hovered:
  1. isPaused becomes true.
  2. All three orbital wrappers receive transition: { duration: 0 }.
  3. The currently active Framer Motion transition is overridden and the rotation freezes.
  4. The satellite tooltip becomes visible via CSS group-hover:opacity-100.
When the modal is open (selectedId !== null), orbits also pause automatically — the condition l || a !== null ensures no satellite continues spinning behind the open modal.

Satellite Icon and Tooltip

Each satellite button is a w-10 h-10 bg-space-800 border border-aurora-teal/50 rounded-lg square with a FileText icon from lucide-react at the centre. On hover:
  • Background transitions to bg-aurora-teal/20.
  • A tooltip div fades in below the icon (top-12, opacity-0 group-hover:opacity-100).
  • The tooltip shows the post title in font-sans text-sm font-bold text-slate-200 and the date in font-mono text-[10px] text-aurora-pink.
The tooltip is w-48 and text-center to handle multi-word titles gracefully, and pointer-events-none so it doesn’t interfere with mouse tracking.

Click-to-Expand Modal

Clicking a satellite sets the selected post ID in state (a). The modal is rendered as a Framer Motion scale entrance:
<motion.div
  initial={{ opacity: 0, scale: 0.9 }}
  animate={{ opacity: 1, scale: 1 }}
  className="w-full max-w-lg glass-panel p-8 rounded-2xl relative"
>
The modal contains five elements:
  1. Close buttonX icon anchored absolute top-4 right-4 inside the glass panel, calls setSelectedId(null) on click.
  2. Datefont-mono text-xs text-aurora-pink at the top of the content area.
  3. Titlefont-sans text-2xl font-bold text-slate-100.
  4. Excerptfont-serif text-lg text-slate-300 leading-relaxed, using the serif typeface for a “transmission text” feel.
  5. CTA button"Download Full Transmission" in font-mono text-sm text-aurora-light, full-width, with aurora-teal border and hover fill.
The “Download Full Transmission” button currently has no href or onClick wired to an actual URL. Connect it to your blog post URL by adding a url field to the post data shape and using it as the href on an <a> tag (or onClick handler for a router navigation).

Adding New Posts

1

Locate the posts array

Open components/OrbitingSatellites.js and find the n array. Each object in the array renders as one orbital satellite.
2

Choose an orbital radius

Existing rings are at 150, 250, and 350px. Add a fourth at 450px — each ring adds 100px, keeping the spacing visually consistent.
{
  id: 4,
  title: "The Gravity of Technical Debt",
  excerpt: "How accumulated shortcuts bend the trajectory of every feature that follows.",
  date: "Stardate 4701.3",
  orbitRadius: 450,
  duration: 65
}
3

Ensure the section is tall enough

The outermost ring’s diameter is orbitRadius * 2. At 450px that’s a 900px-wide ring. The section uses overflow-hidden, so rings wider than the viewport will be clipped — this is intentional on mobile, but test on desktop to verify the outermost ring has visible arc.
4

Wire the CTA button to a real URL

Add a url field to each post object, then update the “Download Full Transmission” button inside the modal to use it:
<a
  href={post.url}
  target="_blank"
  rel="noopener noreferrer"
  className="block w-full py-3 text-center bg-aurora-teal/20 border border-aurora-teal/50 rounded-lg text-aurora-light font-mono text-sm hover:bg-aurora-teal/30 transition-colors"
>
  Download Full Transmission
</a>
All orbits currently rotate clockwise (rotate: 360). To reverse an orbit to counter-clockwise, change its animate value to rotate: -360:
animate={{ rotate: -360 }}
You can mix clockwise and counter-clockwise orbits to add visual complexity — for example, inner orbits clockwise and outer orbits counter-clockwise, mimicking retrograde motion.

Build docs developers (and LLMs) love