Skip to main content

Documentation Index

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

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

Six components provide the ambient atmosphere and interactive focal points that make Spell Index feel alive. Four of them take no props and are mounted globally or per-page; ConstellationText accepts a single text prop; and SummoningForm manages its own form state internally. Each is documented in full below.

BackgroundParticles

BackgroundParticles renders a position: fixed full-viewport layer (z-index: 0) of 40 floating particles. It sits beneath all page content and provides a continuous ambient motion effect throughout the entire app.

How it works

On mount, useMemo generates a stable array of 40 particle descriptors so that layout never triggers re-generation:
const particles = useMemo(
  () =>
    Array.from({ length: 40 }).map((_, i) => ({
      id: i,
      x: Math.random() * 100,          // starting vw position
      y: Math.random() * 100,          // starting vh position
      size: Math.random() * 3 + 1,     // 1–4 px diameter
      duration: Math.random() * 20 + 10, // 10–30 s float cycle
      delay: Math.random() * 5,        // 0–5 s initial delay
      isAmber: Math.random() > 0.8,    // 20 % amber, 80 % teal
    })),
  []
);
Each particle is a motion.div that animates upward by 20 vh and drifts ±5 vw horizontally over its duration, fading from opacity: 0 through 0.4 and back to 0 on an infinite loop. Amber particles glow with shadow-[0_0_8px_rgba(245,196,90,0.6)]; teal particles use shadow-[0_0_8px_rgba(61,214,196,0.4)]. A noise texture SVG and a radial-gradient background (from-ink/40 via-midnight to-void) are layered behind the particles to deepen the starfield appearance.

Usage

import { BackgroundParticles } from "./components/witchy/BackgroundParticles";

// Mount once at the app root, outside all page routes
<BackgroundParticles />

CursorTrail

CursorTrail replaces the default OS cursor with a custom animated orb and a trailing particle cloud. It is mounted globally at the app root level and covers the entire viewport via position: fixed, z-index: 100.
CursorTrail requires cursor: none to be set globally in your CSS so that the system cursor is hidden. Without this, visitors will see both the system arrow and the custom teal orb at the same time. Verify that your global stylesheet includes * { cursor: none; } or body { cursor: none; } before mounting this component. This can impact accessibility — users who rely on high-contrast OS cursors will lose that affordance.

Cursor orb

A motion.div spring-animates to the current mouse position (clientX - 8, clientY - 8) on every mousemove event. When the pointer moves over an interactive element (a, button, input, textarea, [role="button"]), the orb scales to 1.5× and transitions from teal to amber to signal clickability:
animate={{
  x: pos.x - 8,
  y: pos.y - 8,
  scale: isHovering ? 1.5 : 1,
  backgroundColor: isHovering ? "#f5c45a" : "#3dd6c4",
  boxShadow: isHovering
    ? "0 0 20px rgba(245,196,90,0.8)"
    : "0 0 15px rgba(61,214,196,0.8)",
}}
transition={{ type: "spring", stiffness: 500, damping: 28, mass: 0.5 }}

Trail particles

Up to 12 historical mouse positions are stored in state. A setInterval trims the oldest position every 50 ms. Each trail point is rendered via AnimatePresence as a motion.div that immediately begins animating to opacity: 0, scale: 0 and is removed on exit — creating the fading comet-tail effect.

Usage

import { CursorTrail } from "./components/witchy/CursorTrail";

// Mount once at the app root alongside BackgroundParticles
<CursorTrail />

MoonPhases

MoonPhases renders a 256 × 256 px moon SVG whose illuminated area changes in real time as the user scrolls the page. It is placed on the home page as a decorative element that responds to scroll progress.

How it works

Framer Motion’s useScroll tracks the page’s vertical scroll as a MotionValue. A derived useTransform maps scrollY from the range [0, 1000] to [0, 100] (a 0–100 scroll percentage). The component re-renders on each onChange event (clamped 0–100). The moon shape is drawn as a single SVG <path> using a dynamic M/A/A/Z arc formula that morphs between new moon, crescent, half, gibbous, and full moon shapes based on the scrollPercent value:
const getMoonPath = (pct) => {
  const t = pct <= 50 ? pct / 50 : (100 - pct) / 50;
  const sweep = pct < 50 ? 0 : 1;
  const side = 1;
  const rx = 50 * Math.abs(1 - t * 2);
  return `M 50 0 A 50 50 0 1 ${sweep} 50 100 A ${rx} 50 0 1 ${side} 50 0`;
};
A blurred radial parchment glow behind the moon scales with scroll (0.5 → 1.2 → 0.5 via a second useTransform) for extra depth.

Usage

import { MoonPhases } from "./components/witchy/MoonPhases";

<MoonPhases />
MoonPhases uses Framer Motion’s useScroll which measures the document’s root scroll, not a container scroll. It should be placed on a page that has enough content to scroll — it will stay static on pages shorter than the viewport.

CauldronCanvas

CauldronCanvas renders an HTML5 <canvas> animation (400 px tall, full container width) where circular tech-skill bubbles rise from the bottom, wobble side to side, and fade out near the top. A hand-drawn SVG cauldron floats below the canvas in an infinite gentle bob animation.

Canvas bubble system

Ten technology names are hardcoded as the bubble content pool:
const techLabels = [
  "React", "TypeScript", "Node.js", "GraphQL",
  "Next.js", "Tailwind", "Framer Motion",
  "PostgreSQL", "Python", "AWS",
];
Eight bubbles are pre-seeded with randomised vertical positions so the canvas is populated immediately on mount. Each bubble object tracks:
PropertyDescription
xHorizontal spawn position (near canvas centre ± 50 px)
yCurrent vertical position (decrements each frame by speed)
radius20–40 px
speed0.5–1.5 px per frame
textRandom tech label from the pool
wobbleAccumulating angle (radians) for sin drift
wobbleSpeed0.02–0.07 radians per frame
opacityFades in from 0 near the bottom, fades out near the top
When a bubble exits the top (y < -50) or fades to zero, it is reset as a fresh bubble from the cauldron mouth. The canvas is sized to its parent container’s clientWidth on mount and on window resize. A requestAnimationFrame loop drives rendering; the loop is cancelled on component unmount via the effect cleanup function.

Usage

import { CauldronCanvas } from "./components/witchy/CauldronCanvas";

<CauldronCanvas />

ConstellationText

ConstellationText takes a single text string, splits it into individual characters, and renders each as a large Cinzel font letter with an animated amber star dot above it. A dashed SVG polyline connects the star dots in a constellation pattern that draws itself in over 3 seconds.

Props

text
string
required
The string to render as a constellation. Each character becomes an individual animated element. Spaces render as blank gaps. Example: "ALEXANDER".

Animation sequence

  1. The outer motion.div uses variants with staggerChildren: 0.2 and delayChildren: 0.5 so each character enters in sequence.
  2. Each character motion.div starts at { opacity: 0, y: 20, filter: "blur(10px)" } and animates to fully visible and unblurred over 1 s.
  3. The amber star dot above each character pulses scale: 1 → 1.5 → 1 and opacity: 0.5 → 1 → 0.5 in an infinite loop, staggered per character by delay: index * 0.2.
  4. The connecting SVG motion.path animates pathLength: 0 → 1 over 3 s with a 1 s delay, drawing the constellation line after the letters have appeared.

Usage

import { ConstellationText } from "./components/witchy/ConstellationText";

<ConstellationText text="ALEXANDER" />
ConstellationText works best with short strings (5–12 characters). Very long strings will cause letters to overflow their container on mobile — consider using a shorter name or nickname.

SummoningForm

SummoningForm is the contact form component. It renders three fields (name, email, message) on top of an SVG summoning circle that progressively illuminates as the user fills in each field. On successful submission a full-overlay success animation plays.

Summoning circle progress

The SVG behind the form contains three progressive elements that become visible as fields are completed:
Completed fieldsElement revealed
1First triangle (upward-pointing)
2Second triangle (downward-pointing)
3Inner amber circle — full pentagram formed
An amber strokeDashoffset animated circle tracks exact completion percentage: strokeDashoffset: 565 - (565 * completedFields / 3 * 100 / 100).

Form fields

LabelInput typeRequired
True NametextYes
Astral AddressemailYes
Incantationtextarea (4 rows)Yes
The submit button reads “Cast Spell” and is disabled until all three fields have content and while the form is in its 2-second simulated submission state (“Summoning…”). After submission, all fields reset and a success overlay appears for 5 seconds before auto-dismissing.

Usage

import { SummoningForm } from "./components/witchy/SummoningForm";

// Place on the Contact page — no props required
<SummoningForm />
SummoningForm currently uses a client-side setTimeout to simulate form submission. To wire it to a real backend, replace the setTimeout block inside the handleSubmit function with your fetch / API call and resolve the loading/success state accordingly.

Build docs developers (and LLMs) love