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 theDocumentation 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.
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.
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.
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:
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.
- Dangling bug — a
Bugicon (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. - Sliding ghost — a
Ghosticon slides in from the right edge of the screen on a spring withbounce: 0.4. It carries a speech bubble that reads “Boo?” and wobbles on a looping rotation animation.
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.
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.
Nav Bar
The nav bar is rendered conditionally insideLayout.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
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.