Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/apursley2012/full-moon/llms.txt

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

The Cursor component swaps out the browser’s default pointer with a custom three-layer cursor system that matches Full Moon’s witch/dark-fantasy aesthetic. A glowing teal orb trails the pointer with a soft easing lag, a crisp white dot snaps to the exact cursor position for precise targeting, and stochastic spark particles spawn on roughly half of all mouse moves and drift upward before fading out. All three layers are driven by Framer Motion and rendered inside a pointer-events-none container that sits at the very top of the z-stack without ever intercepting clicks.
import { r as React, j as jsx, m as motion } from "../assets/proxy.js";

function Cursor() {
  const [position, setPosition] = React.useState({ x: 0, y: 0 });
  const [particles, setParticles] = React.useState([]);

  React.useEffect(() => {
    let idCounter = 0;

    const handleMouseMove = (e) => {
      setPosition({ x: e.clientX, y: e.clientY });

      // Spawn a trailing spark on ~50% of mouse-move events
      if (Math.random() > 0.5) {
        setParticles((prev) => [
          ...prev.slice(-15),
          { x: e.clientX, y: e.clientY, id: idCounter++ },
        ]);
      }
    };

    window.addEventListener("mousemove", handleMouseMove);
    return () => window.removeEventListener("mousemove", handleMouseMove);
  }, []);

  return (
    <div className="pointer-events-none fixed inset-0 z-[100] overflow-hidden">
      {/* Glowing teal orb — follows cursor with backOut easing */}
      <motion.div
        className="absolute w-4 h-4 rounded-full bg-witch-teal-glow mix-blend-screen blur-[2px]"
        animate={{ x: position.x - 8, y: position.y - 8 }}
        transition={{ type: "tween", ease: "backOut", duration: 0.1 }}
      />

      {/* Precise white dot — snaps to cursor instantly */}
      <motion.div
        className="absolute w-1 h-1 rounded-full bg-white"
        animate={{ x: position.x - 2, y: position.y - 2 }}
        transition={{ type: "tween", ease: "linear", duration: 0 }}
      />

      {/* Trailing spark particles */}
      {particles.map((particle) => (
        <motion.div
          key={particle.id}
          initial={{ opacity: 0.6, scale: 1, x: particle.x, y: particle.y }}
          animate={{
            opacity: 0,
            scale: 3,
            y: particle.y - 20 - Math.random() * 20,
            x: particle.x + (Math.random() - 0.5) * 20,
          }}
          transition={{ duration: 1, ease: "easeOut" }}
          className="absolute w-3 h-3 rounded-full bg-witch-teal-dark mix-blend-screen blur-md"
        />
      ))}
    </div>
  );
}

export { Cursor as C };

Visual Elements

The cursor is composed of three independently animated Framer Motion layers, each serving a distinct perceptual role: 1. Glow orb A 16×16 px (w-4 h-4) circle filled with bg-witch-teal-glow (#2dd4bf). The mix-blend-screen blend mode causes it to lighten any pixels beneath it rather than paint over them, preserving the richness of the dark background while adding a luminous halo effect. A blur-[2px] softens the edge into a diffuse glow. The orb follows the real cursor position offset by 8 px (half its width/height) to center it on the pointer, and uses backOut easing over 100 ms — producing a slight overshoot that makes the glow feel elastic and alive. 2. Precision dot A 4×4 px (w-1 h-1) solid white circle that tracks the cursor with duration: 0 and ease: "linear", meaning it teleports to the new position on every animation frame with zero lag. This gives users a sharp, accurate targeting reference even when the glow orb is still catching up, so interactive elements like buttons and links remain easy to click precisely. 3. Trailing sparks On approximately 50% of mousemove events, a new particle is pushed into the particles array at the current cursor coordinates. Each spark is a 12×12 px (w-3 h-3) circle using bg-witch-teal-dark (#0e3a3a) with mix-blend-screen and heavy blur (blur-md). Its Framer Motion animation runs for 1 second with easeOut timing: opacity falls from 0.6 to 0, scale grows from 1× to 3×, the particle drifts upward between 20–40 px, and lateral jitter of up to ±10 px gives each spark a unique flight path. The combined effect is a short trail of glowing embers that evaporates as the cursor moves.

State

The component manages two pieces of React state:
  • position: { x, y } — Updated on every mousemove event with e.clientX and e.clientY. Both the glow orb and the precision dot derive their animate target from this value.
  • particles: Array<{ x, y, id }> — A rolling buffer of recent spark-spawn positions. Each entry records the cursor coordinates at the moment of spawn and a monotonically incrementing id used as the React key. The array is capped at 15 entries (see Performance Notes below).
Both state updates are wired to a single mousemove listener registered in a useEffect with an empty dependency array, so the listener is attached once on mount and cleaned up on unmount.

Performance Notes

Two deliberate constraints keep the component from degrading performance during fast, continuous mouse movement:
  • Particle cap at 15. Every time a new spark is spawned, the update uses prev.slice(-15) to discard any entries beyond the 15 most recent before appending the new one. Without this guard, rapid cursor movement could push thousands of entries into the array, causing React to reconcile and Framer Motion to animate a growing number of DOM nodes simultaneously.
  • pointer-events-none container. The outermost <div> carries pointer-events-none, which instructs the browser’s hit-testing engine to skip the entire subtree when dispatching pointer events. This means none of the cursor elements — orb, dot, or particles — can accidentally absorb clicks, hovers, or drag events intended for the page content beneath them.

Usage

Cursor is rendered once, unconditionally, inside <Layout>. It takes no props:
import { C as Cursor } from "./Cursor.js";

// Inside Layout — renders the full custom cursor system for the entire site:
// <Cursor />
Because Cursor mounts at the Layout level it is always present regardless of which route is active, so the cursor effect is consistent across every page without any per-page setup.
The custom cursor is only visible on desktop devices. Touch screens and most mobile browsers do not fire mousemove events, so the position state never updates from its initial { x: 0, y: 0 } value. The cursor elements render but remain positioned at the top-left corner of the viewport, outside the visible scroll area, and the pointer-events-none container ensures they have no impact on touch interactions.

Build docs developers (and LLMs) love