Skip to main content

Documentation Index

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

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

The Magical Portfolio is a single-page React application deployed as a collection of static files. At runtime it behaves like a traditional SPA — one JavaScript bundle, one CSS file, client-side routing — but at rest on disk (and on the file-server) it presents a separate HTML entry-point for every URL. This hybrid model means the site works on any static host (GitHub Pages, Netlify, Cloudflare Pages, S3 + CloudFront) without server-side routing support, while still delivering full animated page transitions and a cohesive mystical design system.

Component Hierarchy

The App shell composes four top-level concerns that are always mounted, regardless of which route is active:
<App>
 ├── <BackgroundEffects />       fixed, z-index: -1
 ├── <CustomCursor />            fixed, z-index: 99 / 100
 ├── <Navigation />              fixed, z-index: 50
 └── <AnimatePresence>           Framer Motion exit coordinator
      └── <motion.div>           per-route page wrapper
           └── <PageComponent>  Home | About | Projects | …
LayerComponentz-indexRole
BackgroundBackgroundEffectsz-[-1]Animated star particles, radial gradients, noise texture
CursorCustomCursor (dot)z-[100]12 × 12 px mystic-mint spring-physics dot
CursorCustomCursor (glow)z-[99]128 × 128 px mystic-violet/10 blur halo
NavigationNavigationz-50Fixed header; scrolls into a frosted-glass variant after 50 px
ContentRoute pageautoFull-height page component, padded inside the motion wrapper
BackgroundEffects renders 40 randomly positioned floating particles using Framer Motion’s animate prop with infinite repeat, layered over large blurred radial gradients (blur-[120px] / blur-[150px]) and a near-invisible SVG noise texture. CustomCursor replaces the system cursor on pointer-capable devices (window.matchMedia("(pointer: fine)")). The dot uses spring physics (stiffness: 500, damping: 28, mass: 0.5) so it trails the pointer with a snappy feel. The glow halo uses softer springs (stiffness: 250, damping: 20, mass: 0.8).

Build Pipeline

Vite bundles all React source into three output artefacts consumed by every HTML shim:
assets/
 ├── main.js      # Full application bundle (React, React Router, Framer Motion, Lucide, all pages)
 ├── main.css     # Compiled Tailwind output + custom utilities (.text-glow, scrollbar styles, etc.)
 └── proxy.js     # Re-exports shared runtime values (JSX factory, Framer Motion) to avoid duplication
                  #   across lazy-loaded component chunks

canvas.manifest.js   # Vite asset manifest — maps original source paths → hashed output filenames
Each HTML file also <link rel="modulepreload"> the pre-split component chunks (Navigation.js, BackgroundEffects.js, CustomCursor.js, Sigil.js, and useScreenInit.js) so they are fetched in parallel with main.js before the browser starts parsing the bundle.

Static Deployment Model

Because the app uses hash-based routing (see Routing), every URL the user navigates to is structurally <origin>/pages/About.html#/about — the #/about fragment is consumed entirely by the browser-side React Router and never reaches the server. The static host only ever needs to serve a plain .html file. Each route therefore has its own HTML shim in the pages/ directory:
index.html              →  /         (Portal — home page)
pages/About.html        →  /about
pages/Projects.html     →  /projects
pages/Skills.html       →  /skills
pages/Writing.html      →  /writing
pages/CaseStudies.html  →  /case-studies
pages/Contact.html      →  /contact
Every shim is identical in structure — it mounts a <div id="root">, loads assets/main.js (relative path adjusted for nesting depth), and includes the hash-redirect inline script. The React app boots, reads window.__STATIC_PAGE_ROUTE__ to know which hash fragment to initialise, and React Router takes it from there. This means no 404 rewrites are needed on the host. A user bookmarking /pages/About.html or a search engine following a direct link will always receive a valid HTML document with the correct <title> tag.

Animation Architecture

Framer Motion drives every motion layer in the app:
ConcernAPI UsedExample
Page enter / exitAnimatePresence + motion.divopacity: 0→1, filter: blur(10px)→blur(0px), duration: 0.8 s
Scroll-linked progressuseScroll + useTransformChronicle page timeline bar fills as the user scrolls
Spring physics — cursormotion.div with spring transitionCustomCursor tracks pointer with configurable stiffness / damping
Spring physics — skill orbsmotion.div with type: "spring"Affinity orbs spring into their orbital positions on mount
Path drawing — sigilsmotion.text / motion.path strokeDasharrayHero name strokes in letter-by-letter on the Portal page
3-D tilt — cardsuseMotionValue + useTransformGrimoire and Portal cards tilt on mouse move via rotateX / rotateY
The page-transition wrapper is produced by the App shell and uses AnimatePresence keyed on useLocation().pathname. Both initial and exit states use the same values (opacity: 0, filter: blur(10px)), giving a symmetric cross-fade between routes:
<AnimatePresence mode="wait">
  <motion.div
    key={location.pathname}
    initial={{ opacity: 0, filter: "blur(10px)" }}
    animate={{ opacity: 1, filter: "blur(0px)" }}
    exit={{ opacity: 0, filter: "blur(10px)" }}
    transition={{ duration: 0.8, ease: [0.22, 1, 0.36, 1] }}
  >
    {children}
  </motion.div>
</AnimatePresence>
The ease curve [0.22, 1, 0.36, 1] is a custom cubic-bezier that accelerates quickly at the start and decelerates at the end, giving the blur-in a cinematic, spell-cast feel.

Routing

Hash-based routing, per-route HTML shims, the nav items array, and how to add a new page.

Theming

The mystic-* color palette, typography stack, glow utilities, and Tailwind JIT configuration.

Build docs developers (and LLMs) love