Skip to main content

Documentation Index

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

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

CursorTrail is a purely cosmetic overlay that follows the user’s pointer across the viewport. It paints a teal circle directly under the cursor and leaves behind a dissolving procession of up to five GhostIcon sprites, giving the site a spectral feel without ever interrupting interaction. The component is self-contained: it reads mouse coordinates and the haunted flag from HauntContext, manages its own trail state, and renders nothing on touch devices or when Haunted Mode is off.

Prerequisites

CursorTrail must be rendered inside a HauntProvider so the useHaunt() hook can resolve. It also depends on Framer Motion (AnimatePresence, motion.div) for the ghost fade-out animation.

Rendering conditions

The component performs two checks before rendering anything:
ConditionHow it is checked
Not a touch device'ontouchstart' in window || navigator.maxTouchPoints > 0 detects a touch device — the component renders only when this expression is false
Haunted Mode onisHaunted === true (from useHaunt())
If either check fails the component returns null — no DOM nodes are mounted at all.

Visual structure

When both conditions pass, two layers are rendered: 1 — Cursor dot A fixed 16 × 16 px circle (w-4 h-4) styled with bg-haunt-moon rounded-full mix-blend-screen. It is repositioned on every mousemove via an inline transform: translate(...), offset by −8 px on each axis so the dot is centred on the pointer hotspot.
<div
  className="fixed top-0 left-0 w-4 h-4 bg-haunt-moon rounded-full
             pointer-events-none z-[9999] mix-blend-screen"
  style={{ transform: `translate(${mousePos.x - 8}px, ${mousePos.y - 8}px)` }}
/>
mix-blend-mode: screen (the mix-blend-screen Tailwind utility) means the teal dot adds its luminance to whatever is beneath it rather than covering it, producing a natural glow on dark backgrounds. On light backgrounds the effect will be barely visible.
2 — Ghost trail Each GhostIcon in the trail is wrapped in a Framer Motion motion.div inside AnimatePresence. The icon animates from nearly opaque (opacity: 0.8, scale: 1) to fully transparent and slightly smaller (opacity: 0, scale: 0.5), rising 20 px upward over 500 ms.
<AnimatePresence>
  {trail.map((pos, i) => (
    <motion.div
      key={pos.id}
      initial={{ opacity: 0.8, scale: 1 }}
      animate={{ opacity: 0, scale: 0.5, y: pos.y - 20 }}
      exit={{ opacity: 0 }}
      transition={{ duration: 0.5 }}
      className="fixed top-0 left-0 pointer-events-none z-[9998] text-haunt-moon"
      style={{ transform: `translate(${pos.x}px, ${pos.y}px)` }}
    >
      <GhostIcon className="w-4 h-4" />
    </motion.div>
  ))}
</AnimatePresence>

State management

The trail is held in a single useState array. Each entry is a plain object { id, x, y } where id is a Date.now() timestamp used as the React key and for later removal.
const [trail, setTrail] = useState([]);

useEffect(() => {
  if (!isHaunted || isTouchDevice) return;
  const item = { id: Date.now(), x: mousePos.x, y: mousePos.y };
  setTrail(prev => [...prev.slice(-4), item]); // max 5 items
  const timer = setTimeout(() => {
    setTrail(prev => prev.filter(t => t.id !== item.id));
  }, 500);
  return () => clearTimeout(timer);
}, [mousePos, isHaunted, isTouchDevice]);
prev.slice(-4) keeps at most the four most-recent entries before appending the new one, capping the total trail length at 5 ghosts. The setTimeout schedules removal of each individual item after 500 ms, ensuring ghosts disappear even if the cursor stops moving.

z-index layering

Layerz-indexPurpose
Cursor dot9999Always on top
Ghost trail icons9998Just below the cursor dot
Both layers use pointer-events-none so they never block clicks on the underlying page.
Three values are easy to tweak for different feels:
  • Cursor dot size — change w-4 h-4 on the dot div (e.g. w-6 h-6 for a larger orb).
  • Trail length — change the -4 in .slice(-4) to any number n to keep n + 1 ghosts.
  • Fade duration — change 500 (ms) in the setTimeout and the matching transition.duration (seconds) to speed up or slow down the dissolve.

Build docs developers (and LLMs) love