Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/apursley2012/digital-alchemy/llms.txt

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

Framer Motion is the animation backbone of Digital Alchemy. It drives everything from the site-wide page transition blur-fade to the individual spring-bounce of bookshelf items and the infinite rotation of the crystal ball’s inner orb. All animations are declared directly in JSX using the motion.* component API — no CSS @keyframes required.

Page transition (Layout-level)

The most prominent animation is the blur+opacity fade that plays on every route change. It lives in the Layout component and wraps all page content:
import { AnimatePresence, motion } from 'framer-motion';
import { useLocation } from 'react-router-dom';

const location = useLocation();

<AnimatePresence mode="wait">
  <motion.main
    key={location.pathname}
    initial={{ opacity: 0, filter: 'blur(10px)' }}
    animate={{ opacity: 1, filter: 'blur(0px)' }}
    exit={{ opacity: 0, filter: 'blur(10px)' }}
    transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
    className="flex-1 pt-32 pb-24"
  >
    {children}
  </motion.main>
</AnimatePresence>
mode="wait" on AnimatePresence ensures the exiting page’s animation completes before the entering page begins mounting. Without it, both pages would animate simultaneously, causing a jarring visual overlap.

Component entrance animations

Most sections and cards use a simple fade-up entrance — the content starts slightly below its final position and transparent, then slides up as it fades in:
<motion.div
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ duration: 0.5, ease: 'easeOut' }}
>
  {content}
</motion.div>
For lists of items (skills, projects, testimonials), each item receives a staggered delay based on its index so they cascade in one by one:
<motion.div
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ duration: 0.4, delay: index * 0.08 }}
>
  {item}
</motion.div>

TarotCard flip

The TarotCard component on the About page uses a 3D card flip powered by Framer Motion’s rotateY animation with a spring physics config:
<motion.div
  animate={{ rotateY: isFlipped ? 180 : 0 }}
  transition={{
    type: 'spring',
    stiffness: 60,
    damping: 15,
  }}
  style={{ transformStyle: 'preserve-3d' }}
>
  <div className="card-face card-front">{front}</div>
  <div className="card-face card-back">{back}</div>
</motion.div>
The stiffness: 60 and damping: 15 values produce a gentle, slightly bouncy flip that feels physical without being distracting.

Bookshelf spring bounce

On the Skills page, the Bookshelf component renders skill books that spring into place with a bounce. Books are staggered so they pop in one after another:
<motion.div
  initial={{ opacity: 0, y: 40, scaleY: 0.8 }}
  animate={{ opacity: 1, y: 0, scaleY: 1 }}
  transition={{
    type: 'spring',
    bounce: 0.4,
    delay: index * 0.07,
  }}
>
  {book}
</motion.div>
bounce: 0.4 is the shorthand for a spring that overshoots its target slightly — perfect for books snapping upright onto a shelf.

CrystalBall ambient animation

The CrystalBall component on the Projects page uses an infinite looping animation to create a breathing, rotating inner orb that gives the ball a sense of life:
<motion.div
  animate={{
    scale: [1, 1.2, 1],
    rotate: [0, 180, 360],
    opacity: [0.6, 1, 0.6],
  }}
  transition={{
    duration: 20,
    ease: 'linear',
    repeat: Infinity,
  }}
  className="crystal-inner-orb"
/>
The 20-second duration keeps the motion subtle enough to feel ambient rather than distracting.

SpellForm rune particles

The SpellForm contact component releases rune particles when a spell (message) is cast. Each particle is animated along a random upward trajectory and loops infinitely while the form is active:
// x is a random offset between -30 and +30 pixels
const x = (Math.random() - 0.5) * 60;

<motion.span
  initial={{ opacity: 0, y: 20, x: x }}
  animate={{ opacity: [0, 1, 0], y: -100, x: x }}
  transition={{
    duration: 2,
    ease: 'easeOut',
    repeat: Infinity,
    delay: Math.random() * 2,
  }}
  className="rune-particle"
>
  {rune}
</motion.span>
Each particle gets a random delay so they don’t all trigger simultaneously, creating a natural staggered stream effect.
The navigation active-state dot uses Framer Motion’s shared layout animation to slide smoothly between nav items without requiring any manual position calculation:
// On the active nav item only
<motion.span
  layoutId="nav-indicator"
  className="absolute -bottom-1 left-1/2 w-1 h-1 rounded-full bg-turquoise"
/>
Because all nav items share the same layoutId="nav-indicator", Framer Motion automatically animates the dot from the previous active item to the new one whenever the route changes. The transition is handled entirely by Framer Motion’s layout engine.

Animation summary

ComponentPatternKey props
Page transitionBlur + opacity fadeduration: 0.6, ease [0.22, 1, 0.36, 1]
Section entrancesFade-upy: 20 → 0, staggered delays
TarotCard3D fliprotateY, spring stiffness: 60, damping: 15
BookshelfSpring bouncetype: spring, bounce: 0.4
CrystalBallAmbient scale + rotate20s, repeat: Infinity
SpellForm particlesFloat-up loopy: -100, 2s, repeat: Infinity, random delay
Nav indicatorShared layoutlayoutId="nav-indicator"

Build docs developers (and LLMs) love