Skip to main content

Documentation Index

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

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

Motion is central to the Windows XP Developer Portfolio’s character. The goal is not decoration for its own sake but the same principle that guided the original Windows XP design team: animation should make the interface feel alive and give users clear feedback about what is happening. Every panel slides into view, every window springs open, menus breathe in and out, and testimonial cards drift as if suspended. This is all orchestrated by Framer Motion, the React animation library that powers every dynamic element in the portfolio.

Animation library

Framer Motion is consumed via the motion re-export bundled in assets/proxy.js:
// assets/proxy.js re-exports framer-motion's motion object
import { motion as m } from 'framer-motion';
export { m };
Every animated component in the project imports m (aliased from motion) from this proxy rather than directly from framer-motion, keeping the bundle deduplication clean. Use motion.div, motion.button, etc. by replacing the HTML tag with the m.* equivalent:
import { m } from '../assets/proxy.js';

// Unanimated:
<div className="aqua-glass"></div>

// Animated:
<m.div className="aqua-glass" initial={{ opacity: 0 }} animate={{ opacity: 1 }}>

</m.div>

Entrance animations

AquaPanel

AquaPanel is the most frequently rendered animated element in the portfolio. Every instance plays a fade + upward slide entrance whose delay is controlled by the delay prop, allowing staggered reveals when multiple panels mount together.
// AquaPanel.js
<m.div
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ duration: 0.5, delay: delay, ease: 'easeOut' }}
  className={`${heavy ? 'aqua-glass-heavy' : 'aqua-glass'} rounded-2xl p-6 ${className}`}
>
  {children}
</m.div>
Pass incrementing delay values to a list of AquaPanel components to create a cascade effect:
{sections.map((section, i) => (
  <AquaPanel key={section.id} delay={i * 0.1}>
    {section.content}
  </AquaPanel>
))}

AquaWindow

AquaWindow uses a spring-based scale + fade entrance. The spring physics (damping: 25, stiffness: 300) produce a subtle overshoot that evokes the “pop open” feel of a native window appearing on screen. The same animation runs in reverse as an exit.
// AquaWindow.js
<m.div
  initial={{ scale: 0.9, opacity: 0 }}
  animate={{ scale: 1, opacity: 1 }}
  exit={{ scale: 0.9, opacity: 0 }}
  transition={{ type: 'spring', damping: 25, stiffness: 300 }}
  className="flex flex-col rounded-t-xl rounded-b-md shadow-2xl …"
>
  {/* title bar + content */}
</m.div>
The exit prop only fires when AquaWindow is wrapped in an AnimatePresence boundary. Without AnimatePresence the exit animation is skipped and the component unmounts immediately.

Start Menu entrance

The StartMenu overlay uses a combined translate + scale + fade animation tuned for a snappy 200 ms duration. The slight scale from 0.95 → 1 gives the panel a “zooming in from the taskbar” feel.
// StartMenu entrance / exit
<m.div
  initial={{ opacity: 0, y: 20, scale: 0.95 }}
  animate={{ opacity: 1, y: 0, scale: 1 }}
  exit={{ opacity: 0, y: 20, scale: 0.95 }}
  transition={{ duration: 0.2 }}
>
  {/* menu content */}
</m.div>
The StartMenu wraps its animated content in AnimatePresence so that when the user closes the menu, the exit animation (opacity: 0, y: 20, scale: 0.95) plays to completion before the component is removed from the DOM:
<AnimatePresence>
  {isOpen && (
    <m.div
      initial={{ opacity: 0, y: 20, scale: 0.95 }}
      animate={{ opacity: 1, y: 0, scale: 1 }}
      exit={{ opacity: 0, y: 20, scale: 0.95 }}
      transition={{ duration: 0.2 }}
    >
      <StartMenuContent />
    </m.div>
  )}
</AnimatePresence>

Testimonial floating animation

Testimonial cards use a looping vertical drift animation that gives each card the appearance of floating in zero gravity. The delay value per card is sourced from the card’s data object, staggering the phase of each float so cards bob independently rather than in unison.
// Testimonials page — per-card floating motion
<m.div
  animate={{ y: [0, -15, 0] }}
  transition={{
    duration: 4,
    repeat: Infinity,
    ease: 'easeInOut',
    delay: card.delay,
  }}
>
  <TestimonialCard card={card} />
</m.div>
The keyframe array [0, -15, 0] drives the card 15 px upward then back to its origin over each 4-second cycle. easeInOut smooths both the rise and fall so the motion feels natural rather than mechanical.

CD-flip case study (3D)

The Case Studies page features the most complex animation in the portfolio: a 3D CD jewel-case flip. When a case study is opened, the cover panel rotates −160° around its left edge as if a CD case is being opened, revealing the content inside. The animation uses Framer Motion’s rotateY with spring physics to give the hinge a physical weight.
// CaseStudies page — 3D flip
<m.div
  animate={{ rotateY: isOpen ? -160 : 0 }}
  transition={{
    duration: 0.8,
    type: 'spring',
    stiffness: 50,
  }}
  style={{
    transformStyle: 'preserve-3d',
    transformOrigin: 'left center',
  }}
>
  <CDCover />
</m.div>

transformStyle: preserve-3d

Required on the parent so that child elements participate in the same 3D rendering context. Without this, rotateY flattens to a 2D scale effect.

transformOrigin: left center

Anchors the rotation to the left edge, exactly like a physical hinge, so the cover swings open to the right.
A low stiffness: 50 (compared to the 300 used for AquaWindow) produces a slow, weighty swing — appropriate for the heavier feel of a physical object rather than a lightweight UI panel.

CSS lens-flare animation

Not all animation in the portfolio goes through Framer Motion. The lens-flare class uses a pure CSS ::after pseudo-element and a transition to sweep a white highlight across an element on hover:
/* assets/main.css */
.lens-flare::after {
  content: "";
  position: absolute;
  top: -50%;
  left: -50%;
  width: 200%;
  height: 200%;
  background: linear-gradient(
    to bottom right,
    transparent,
    transparent 40%,
    rgba(255, 255, 255, 0.6),
    transparent 60%,
    transparent
  );
  transform: rotate(30deg) translate(-100%, -100%); /* off-screen left */
  transition: transform 0.6s ease-in-out;
  pointer-events: none;
}

.lens-flare:hover::after {
  transform: rotate(30deg) translate(100%, 100%); /* sweeps to off-screen right */
}
For list-item hover groups, the same sweep is also available as a named @keyframes sweep animation applied via group-hover:animate-[sweep_1s_ease-in-out]:
/* Triggered by a parent element carrying the `group` class */
.group:hover .group-hover\:animate-\[sweep_1s_ease-in-out\] {
  animation: sweep 1s ease-in-out;
}

Button tap interaction

AquaButton combines the CSS :active translateY(+2px) press with Framer Motion’s whileTap to add a simultaneous scale squeeze:
// AquaButton.js
<m.button
  whileTap={{ scale: 0.95 }}
  className="aqua-button-base lens-flare …"
  {...props}
>
  <span className="relative z-10 drop-shadow-sm">{children}</span>
</m.button>
The scale: 0.95 spring snaps back naturally when the pointer is released, giving a tactile “click” sensation layered on top of the CSS press transform.

AnimatePresence usage summary

AnimatePresence must wrap any conditionally rendered motion.* element that should play an exit animation. The table below lists where it is used in the portfolio.
ComponentWhat it gatesExit animation
StartMenuThe menu overlay panelopacity: 0, y: 20, scale: 0.95 over 200 ms
AquaWindow (conditional render)The window containerscale: 0.9, opacity: 0 via spring
Route transitionsPage-level componentsDelegated to each page’s root motion element
AnimatePresence only works with direct children that are conditional. If the animated motion.* element is nested deeper inside a non-animated wrapper, the exit animation will not fire. Keep the m.* element as the immediate child of AnimatePresence.

Build docs developers (and LLMs) love