Skip to main content

Documentation Index

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

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

Full Stack Sorcery’s atmosphere isn’t just visual polish — it’s driven by three purpose-built ambient components that run continuously in the background on every page. Each lives in the components/ directory, is imported into Layout.js, and renders independently of route changes. Together they create the fog layer, the ghost cursor, and the occasional jump scare that make the portfolio feel like an actual haunted site.

FogBackground

components/FogBackground.js renders a persistent, full-viewport fog overlay that sits behind all page content. What it does: Two overlapping <div> layers are absolutely positioned to fill the viewport. Each layer is twice the width of the screen (w-[200%]) and carries a radial-gradient background image tiled horizontally. Both layers scroll horizontally on an infinite loop via the animate-fog-scroll CSS class, but at different durations and directions — one forward, one reversed — creating a natural parallax drift. CSS animation: The fog-scroll keyframe is defined in assets/main.css and simply translates the element from its natural position to −50% on the X axis. Because the element is 200% wide and the background tiles at 50% width, the scroll is perfectly seamless.
@keyframes fog-scroll {
  0%   { transform: translate(0); }
  100% { transform: translate(-50%); }
}

.animate-fog-scroll {
  animation: fog-scroll 60s linear infinite;
}
Two-layer parallax: The first layer uses the default 60 s duration (set by animate-fog-scroll) and a teal radial gradient at 15% opacity. The second layer overrides animationDuration to 40s and sets animationDirection: "reverse", so it scrolls in the opposite direction at a different speed. The resulting interference pattern reads as a slowly churning mist. The container itself has opacity-30 and mix-blend-screen applied so the fog blends with the dark midnight background without obscuring text.
// Simplified structure from FogBackground.js
<div className="fixed inset-0 pointer-events-none z-0 overflow-hidden opacity-30 mix-blend-screen">
  {/* Layer 1 — 60s, forward */}
  <div
    className="absolute inset-0 w-[200%] h-full animate-fog-scroll"
    style={{
      backgroundImage: "radial-gradient(ellipse at center, rgba(63,214,192,0.15) 0%, rgba(10,42,47,0) 70%)",
      backgroundSize: "50% 100%",
      backgroundRepeat: "repeat-x",
    }}
  />
  {/* Layer 2 — 40s, reversed, scaleY(1.5) */}
  <div
    className="absolute inset-0 w-[200%] h-full animate-fog-scroll opacity-50"
    style={{
      animationDuration: "40s",
      animationDirection: "reverse",
      backgroundImage: "radial-gradient(ellipse at center, rgba(94,234,212,0.1) 0%, rgba(10,42,47,0) 50%)",
      backgroundSize: "30% 100%",
      backgroundRepeat: "repeat-x",
      transform: "scaleY(1.5)",
    }}
  />
</div>

CustomCursor

components/CustomCursor.js replaces the browser’s default cursor with an animated ghost SVG icon (the Ghost icon from lucide-react) that follows the mouse using Framer Motion spring physics. What it does: The component tracks mousemove events and feeds the raw clientX/clientY values into Framer Motion MotionValues. A spring-damped useSpring hook smooths those values so the ghost trails behind the pointer with a slight lag, giving it a floaty, ethereal feel. A motion.div renders the ghost icon at the spring-smoothed position. Cursor hiding: The default cursor is hidden globally via cursor: none on body in assets/main.css. The CustomCursor component renders at z-[9999] with pointer-events-none so it never interferes with click targets. Reduced-motion exception: assets/main.css includes a media query that restores the native cursor when the user has requested reduced motion:
@media (prefers-reduced-motion: reduce) {
  body {
    cursor: auto;
  }
}
The component also reads window.matchMedia("(prefers-reduced-motion: reduce)") at mount time and returns null early if it matches, so the ghost element is never inserted into the DOM at all for users who have opted out. Hover state: A mouseover listener checks whether the hovered element is an interactive target (a, button, input, textarea, select, or [role="button"]). When it is, the ghost scales up to 1.5×, rotates 10°, and shifts colour from #3fd6c0 to #5eead4 via a Framer Motion spring animation — a subtle cue that something is clickable. Visibility: The ghost fades in on the first mousemove event (opacity transitions from 0 to 1) and fades out when the mouse leaves the document via mouseleave.

JumpScareController

components/JumpScareController.js is an idle-triggered animation system that occasionally startles the user after they’ve stopped interacting with the page. What it does: The component sets a 20-second idle timer on every mousemove and click event. Once that timer fires without interruption, an animated ghost and a dangling spider-bug icon drop into the viewport. The idle state is tracked with useState and the timer is managed with setTimeout/clearTimeout on a useRef ref.
Idle timeout:    20 000 ms  (20 seconds of no mouse movement or clicks)
Scare duration:  4 000 ms   (4 seconds before the ghost exits)
Two simultaneous animations:
  1. Dangling bug — a Bug icon (lucide-react) drops from above on a thin vertical line, landing near the last known mouse position. It uses a Framer Motion spring (damping: 12, stiffness: 100) so it bounces on arrival.
  2. Sliding ghost — a Ghost icon slides in from the right edge of the screen on a spring with bounce: 0.4. It carries a speech bubble that reads “Boo?” and wobbles on a looping rotation animation.
Both animations are wrapped in AnimatePresence so they cleanly exit when the idle state clears. Click counter scare: Separately, every third click (tracked via a counter in useState) triggers a shorter “peek” animation regardless of idle state — keeping users on their toes even during active browsing. Reduced-motion: The component calls window.matchMedia("(prefers-reduced-motion: reduce)") at mount. If the media query matches, the entire component returns null and no jump-scare elements are ever rendered.
The jump scare fully respects prefers-reduced-motion. Users who have enabled this OS-level accessibility setting will never see the idle animation or the click-counter peek.
Homepage tip: The home page (/) displays the hint text: “Tip: idle too long and something might appear.” — a gentle heads-up so the animation doesn’t feel genuinely alarming.
The nav bar is rendered conditionally inside Layout.js — only when location.pathname !== "/". On the home route it is hidden entirely, keeping the landing page uncluttered. When visible, the nav bar is fixed at the top of the viewport (z-50) with a bg-gradient-to-b from-spooky-midnight to-transparent so it fades into the page content below. It has two regions:
  • SpookyDev logo — a React Router <Link to="/"> that returns to the home page
  • Icon + label links — a horizontally scrollable row of <NavLink> elements, one for each of the eight non-home routes
Each nav link renders its lucide-react icon above its spooky label, making the bar both compact and expressive. The scrollable row means the full set of links remains accessible on narrow viewports without wrapping.
All four ambient components — FogBackground, CustomCursor, JumpScareController, and the nav bar — are imported and composed inside a single Layout component in components/Layout.js. Every route in the app is wrapped by Layout, so the ambient effects are always present regardless of which page the user is on.

Build docs developers (and LLMs) love