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 TelemetryRadar component renders your technical skills as signals on a radar scope — a rotating sweep arm continuously circles the display, and each skill “pings” with a mint glow when the sweep crosses its position. Below the radar, a grid of skill cards shows the same data as animated progress bars that fill into view on scroll. The result is a two-layer visualization: spatial intuition on the radar, precise percentages in the cards.
The radar sweep is driven by requestAnimationFrame rather than a CSS animation or Framer Motion transition. This gives frame-accurate sweep-angle tracking so the ping detection logic can fire reliably at the exact moment the sweep crosses each skill’s angular position.

Skill Data Shape

All skills are stored in a static array (d in the compiled source). Each entry must conform to this object shape:
{
  name: string,      // Display name, e.g. "React"
  angle: number,     // Angular position on the radar in degrees (0–360)
  distance: number,  // Radial distance from the centre (0–100 scale)
  strength: number   // Proficiency percentage shown in the progress bar (0–100)
}

Pre-configured Skills

The component ships with eight skills pre-positioned around the radar:
SkillAngleDistanceStrength
React30°4095%
TypeScript75°6090%
Tailwind120°3598%
Framer Motion160°7085%
Node.js210°5580%
Three.js260°8070%
GraphQL310°6575%
UI/UX Design345°4588%
Angles increase clockwise from the 12 o’clock position. The distance value maps to a percentage of the radar’s radius, so a distance of 50 places the dot exactly halfway between the centre and the outer ring, while 80 (like Three.js) places it near the edge.

Radar Structure

The radar container is a circular aspect-square div with a faint border-aurora-teal/20 border and a dark bg-space-900/50 fill. Inside it, four layers are stacked:

Concentric Rings

Four absolute rounded-full border border-aurora-teal/10 divs sized at 25%, 50%, 75%, and 100% of the container. These represent the range rings visible on real radar displays.

Crosshair Lines

A full-width horizontal div and a full-height vertical div, each 1px thick with bg-aurora-teal/10, forming the cardinal axis lines at the radar’s centre.

Sweep Wedge

A conic-gradient div (w-1/2 h-1/2) positioned in the top-left quadrant of the radar container, with origin-bottom-right so its rotation pivot sits at the radar’s centre. Its rotate CSS transform is updated each frame via React state, creating the illusion of a rotating sweep arm.

Skill Dots

Each skill is an absolutely positioned dot whose left and top percentage positions are calculated from its angle and distance polar coordinates.

Polar-to-CSS Coordinate Conversion

Each skill’s radar position is derived from its angle (degrees) and distance (0–100 scale) using standard polar-to-Cartesian maths, mapped to percentage-based CSS positioning:
const angleRad = skill.angle * (Math.PI / 180);
const left = 50 + (skill.distance / 2) * Math.cos(angleRad); // % from left
const top  = 50 + (skill.distance / 2) * Math.sin(angleRad); // % from top
The / 2 factor scales the distance value (0–100) into half the container’s width, keeping all dots within the radar circle.

Sweep Animation & The Ping Effect

The sweep angle is stored in React state (n) and incremented every animation frame:
// useEffect — sets up the rAF loop
useEffect(() => {
  let frameId;
  const tick = () => {
    setSweepAngle(prev => (prev + 1) % 360);
    frameId = requestAnimationFrame(tick);
  };
  frameId = requestAnimationFrame(tick);
  return () => cancelAnimationFrame(frameId);
}, []);
For each skill, the component checks whether the sweep has recently passed its angular position. A dot is “active” (pinging) when the trailing distance between the current sweep angle and the skill’s angle falls within a 30° window:
const trailingDelta = (sweepAngle - skill.angle + 360) % 360;
const isPinging = trailingDelta > 0 && trailingDelta < 30;
When isPinging is true, the dot switches from bg-aurora-teal/40 to bg-aurora-mint with a bright shadow-[0_0_15px_rgba(52,211,153,1)] glow, and a Framer Motion animate prop pulses its scale:
// Framer Motion animate on the dot
animate={isPinging ? { scale: [1, 1.5, 1] } : { scale: 1 }}
Because the sweep increments by 1° per frame, the ping window of 30° is visible for approximately 30 frames — roughly half a second at 60fps. If you increase the increment step for a faster sweep, consider widening the ping window proportionally so users can still perceive the flash.

Hover Tooltip

Each skill dot is wrapped in a group div. A tooltip overlay is absolutely positioned below the dot (top: 4) with opacity-0 group-hover:opacity-100 transition-opacity. The tooltip displays:
  • The skill name in aurora-light monospace.
  • The strength percentage in aurora-pink, separated by a space.
<div className="glass-panel px-3 py-1 rounded text-xs font-mono text-aurora-light">
  {skill.name} <span className="text-aurora-pink ml-2">{skill.strength}%</span>
</div>

Progress Bar Grid

Below the radar, a grid grid-cols-2 md:grid-cols-4 lays out one card per skill. Each card contains:
  1. Skill nametext-sm font-sans text-slate-300
  2. Progress trackw-full h-1 bg-space-800 rounded-full overflow-hidden
  3. Animated fill bar — a Framer Motion div that animates from width: 0 to width: {strength}% when the card scrolls into view:
<motion.div
  className="h-full bg-gradient-to-r from-aurora-teal to-aurora-mint"
  initial={{ width: 0 }}
  whileInView={{ width: `${skill.strength}%` }}
  viewport={{ once: true }}
  transition={{ duration: 1, delay: 0.2 }}
/>
The viewport: { once: true } flag means each bar animates only the first time it enters the viewport — it will not re-animate on subsequent scrolls.

Adding and Editing Skills

1

Locate the skills array

Open components/TelemetryRadar.js and find the d array near the top of the file.
2

Append a new skill entry

Add a new object with a unique name, an angle (0–359°), a distance (10–85 recommended to stay within the radar circle visually), and a strength (0–100).
{ name: "Rust", angle: 50, distance: 72, strength: 60 }
3

Avoid overlapping positions

Check existing angles before placing a new dot. Skills whose angles differ by fewer than 20° may have overlapping tooltips. Adjust angle or distance to give each dot visual breathing room.
4

Verify the progress bar grid

The skill cards grid uses md:grid-cols-4. With more than 8 skills, the grid naturally wraps — no changes needed. With fewer than 4, consider switching to md:grid-cols-3 to avoid orphaned cells.
The sweep speed is controlled by the increment value inside the requestAnimationFrame callback. The default increments by 1 degree per frame:
setSweepAngle(prev => (prev + 1) % 360)
  • Faster sweep: increase to 2 or 3 degrees per frame.
  • Slower sweep: use a fractional accumulator pattern — accumulate a float and only update state when a full degree is crossed, or throttle with a timestamp check.
  • Pause on hover: add a isPaused ref and skip the setSweepAngle call when isPaused.current is true. Toggle it in onMouseEnter/onMouseLeave on the radar container.

Build docs developers (and LLMs) love