Skip to main content

Documentation Index

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

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

The CandleTimeline component presents the portfolio’s work history as a row of physical wax candles. Each candle represents one job: its height is proportional to how long that role lasted, a Framer Motion flame flickers at the top of each wick, and the job details (title, company, period, description) float above the candle body in a card that lifts on hover. The component accepts no props — all data is stored internally.

Hardcoded work data

Three work experiences are defined as a const array at the top of CandleTimeline.js:
const experiences = [
  {
    company: "Mystic Tech Ltd",
    title: "Senior Alchemist (Frontend)",
    period: "2021 - Present",
    description:
      "Leading a coven of developers in building a next-generation " +
      "spellcasting platform (SaaS dashboard). Reduced load times by 40%.",
    durationYears: 3,
  },
  {
    company: "Ethereal Agency",
    title: "Web Sorcerer",
    period: "2018 - 2021",
    description:
      "Crafted bespoke digital experiences for high-profile clients. " +
      "Mastered the dark arts of CSS animations and WebGL.",
    durationYears: 3,
  },
  {
    company: "Startup Void",
    title: "Junior Code Weaver",
    period: "2016 - 2018",
    description:
      "Survived the chaotic early days of a startup. Built REST APIs " +
      "and learned that console.log is a developer's best friend.",
    durationYears: 2,
  },
];

Candle height calculation

Candle height in pixels is calculated from durationYears using a linear scale clamped to a minimum of 100 px and a maximum of 250 px:
const candleHeight = Math.max(100, Math.min(250, durationYears * 60));
durationYearsRaw (× 60)Clamped height
160 px100 px (min)
2120 px120 px
3180 px180 px
4240 px240 px
5+300+ px250 px (max)
The height is applied as an inline style on the candle body div:
<div
  className="w-12 rounded-t-sm bg-gradient-to-b from-[#e8dcc7] to-[#b8a890]
             shadow-[inset_-4px_0_10px_rgba(0,0,0,0.2)]"
  style={{ height: `${candleHeight}px` }}
>

Animated flame

Each candle has a three-layer flame assembly mounted above the wick:
{/* Outer glow — amber radial blur, infinite opacity pulse */}
<motion.div
  className="absolute bottom-0 w-16 h-16 bg-amber/20 rounded-full blur-xl"
  animate={{ opacity: [0.3, 0.5, 0.3] }}
  transition={{ duration: 2, repeat: Infinity }}
/>

{/* Main flame shape — amber teardrop, scale + rotate flicker */}
<motion.div
  className="absolute bottom-0 w-4 h-8 bg-amber rounded-[50%_50%_20%_20%]
             blur-[2px] mix-blend-screen"
  animate={{
    scale:   [1, 1.1, 0.9, 1.05, 1],
    rotate:  [0, -2, 3, -1, 0],
    opacity: [0.8, 1, 0.7, 0.9, 0.8],
  }}
  transition={{ duration: 0.5, repeat: Infinity, ease: "easeInOut" }}
/>

{/* Inner white core */}
<div className="absolute bottom-0 w-2 h-4 bg-white rounded-full blur-[1px]" />
The short 0.5 s cycle on the main flame combined with the slower 2 s glow pulse creates naturalistic flickering without feeling mechanical.

Layout and entrance animation

All three candles sit in a flex-col md:flex-row container, switching from a vertical mobile stack to a side-by-side desktop layout at md breakpoint. Each candle group is a motion.div that animates into view using whileInView:
<motion.div
  initial={{ opacity: 0, y: 50 }}
  whileInView={{ opacity: 1, y: 0 }}
  viewport={{ once: true }}
  transition={{ duration: 0.8, delay: index * 0.3 }}
>
The viewport={{ once: true }} flag ensures the entrance fires only on first scroll-into-view, not on every scroll-up/scroll-down cycle. A thin horizontal gradient rule (via-ink) runs along the bottom of the container to represent the table surface the candles rest on.

Wax drip details

Three SVG-styled div elements are absolutely positioned on the candle body to simulate wax drip streaks:
{/* Left drip */}
<div className="absolute top-0 left-1 w-2 h-6 bg-[#e8dcc7] rounded-b-full
                shadow-[inset_-1px_0_2px_rgba(0,0,0,0.1)]" />
{/* Right drip */}
<div className="absolute top-0 right-2 w-1.5 h-10 bg-[#e8dcc7] rounded-b-full
                shadow-[inset_-1px_0_2px_rgba(0,0,0,0.1)]" />
{/* Center drip */}
<div className="absolute top-0 left-4 w-1 h-4 bg-[#e8dcc7] rounded-b-full" />
Below the candle, a blurred oval shadow (w-16 h-2 bg-void blur-sm) gives the illusion that each candle is resting on the surface.

Usage

import { CandleTimeline } from "./components/witchy/CandleTimeline";

// Drop directly onto the Work page — no props required
<CandleTimeline />

Adding a new work experience

To add a new job, append an entry to the experiences array at the top of CandleTimeline.js:
{
  company: "Arcane Consultancy",
  title: "Principal Enchanter",
  period: "2024 - Present",
  description:
    "Building AI-powered portfolio generators for wizards worldwide. " +
    "Reduced time-to-grimoire by 70%.",
  durationYears: 1,
},
Candle heights are automatically recalculated from durationYears — you do not need to set a height manually. The layout adapts to any number of entries: the flex container wraps on mobile and spreads evenly on desktop.

Build docs developers (and LLMs) love