Skip to main content

Documentation Index

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

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

GhostMascot is a decorative floating ghost that idles with a gentle up-and-down float animation, giving the portfolio a playful haunted quality. The ghost is drawn as an inline SVG and animates its own body shape using Framer Motion’s motion.path morph, making it feel alive even while at rest.

Float animation

The gentle vertical float is provided by the animate-float CSS class, defined in main.css:
@keyframes float {
  0%, 100% { transform: translateY(0); }
  50%       { transform: translateY(-10px); }
}
.animate-float {
  animation: float 4s ease-in-out infinite;
}
The ghost rises 10px upward at the midpoint of each 4-second ease-in-out cycle, then returns to its baseline, looping indefinitely. The ease-in-out timing function makes the movement feel buoyant rather than mechanical. In addition to the container-level float, the ghost’s body path itself is animated with Framer Motion between two slightly different d attribute values — the wavy bottom hem shifts slightly, so the ghost’s hem ripples in and out of phase with the vertical float:
animate={{
  d: [
    "M10 50 C10 20, 90 20, 90 50 L90 110 C80 100, 70 120, 60 110 C50 100, 40 120, 30 110 C20 100, 10 120, 10 110 Z",
    "M10 50 C10 25, 90 25, 90 50 L90 115 C80 105, 70 125, 60 115 C50 105, 40 125, 30 115 C20 105, 10 125, 10 115 Z",
    "M10 50 C10 20, 90 20, 90 50 L90 110 C80 100, 70 120, 60 110 C50 100, 40 120, 30 110 C20 100, 10 120, 10 110 Z",
  ],
}}
transition={{ repeat: Infinity, duration: 2, ease: "easeInOut" }}

Ghost anatomy

The SVG renders on an 80×100 px canvas (viewBox="0 0 100 120"). It consists of:
  • Body — a motion.path with a cubic-Bézier dome top and wavy hem, filled #f5f5f5 (the --color-ghost CSS variable)
  • Eyes — two solid <circle> elements at cx=35, cy=45 and cx=65, cy=45 with radius 5, filled #1a1a1d
  • Rosy cheeks — two <circle> elements at the sides of the face, filled #ff7518 (pumpkin orange) at opacity: 0.4
  • Mouth — a small quadratic Bézier arc M45 60 Q50 65 55 60 stroked in #1a1a1d with strokeLinecap: round

Jump-scare context

GhostMascot consumes JumpScareContext via the useJumpScare() hook. It uses a setInterval that fires every 15 seconds; each tick has a 50% chance of triggering a visible state. When triggered, visible is set to true and the ghost enters from the right side of the screen via a Framer Motion spring (stiffness: 100, damping: 15), stays visible for 3 seconds, then exits. If scaresEnabled is false, the interval is never started.

Placement

GhostMascot is positioned fixed bottom-20 right-0 at z-40, rendering in the lower-right corner of the viewport. The outer container carries pointer-events-none so it never intercepts clicks. It is rendered via the Layout component and therefore appears on every page.

Customizing the mascot

  • Float distance — change -10px in the @keyframes float rule to increase or decrease how high the ghost rises on each cycle
  • Float speed — change 4s in .animate-float to speed up or slow down the bob
  • Body morph — adjust the d array values in the Framer Motion animate prop to change how dramatically the hem ripples
  • Colors — the fill #f5f5f5 and rosy-cheek #ff7518 colors are set directly on SVG elements; replace them to change the ghost’s appearance
The ghost mascot is one of Spooky’s signature personality touches. If you prefer a different mascot illustration — a bat, a skull, or a custom character — you can replace the SVG markup inside GhostMascot.js after rebuilding from source. Keep the animate-float class on the outer container and the Framer Motion AnimatePresence wrapper to retain the entrance and float behaviour.
The animate-float animation can be disabled for users who prefer reduced motion using the same pattern as the flicker animation:
@media (prefers-reduced-motion: reduce) {
  .animate-float {
    animation: none;
  }
}
You should also guard the Framer Motion path morph animation by reading useReducedMotion() from Framer Motion and setting the animate prop to the static initial value when it returns true.

Build docs developers (and LLMs) love