import { useLayoutEffect, useRef } from "react"
import { ArrowRight } from "lucide-react"
type TimelinePoint = {
percentage: number
label: string
subtitle: string
details: string[]
variant: "left" | "right"
mobileLabel?: string
mobileSubtitle: string
mobileDetails: string[]
}
type PointPosition = {
x: number
y: number
}
export type Timeline01Props = {
className?: string
}
const PATH_D =
"M1 2C299.481 2 593.937 49.2507 835.5 142.213 1143.67 260.81 1365.77 453.804 1400.3 718 1403.41 741.749 1405 766.074 1405 790.972C1405 1015.35 1206.5 1177.61 971 1343.12 752.77 1496.48 502.766 1652.64 349.5 1863.6 261.584 1984.61 205.499 2123.65 205.499 2290.54C205.499 2453.35 257.153 2581.89 336.864 2690.5 493.17 2903.47 757.359 3039.79 951.5 3207.49 1091.79 3328.68 1195.5 3466.25 1195.5 3660.98C1195.5 3836.03 1099.14 3925.77 980.477 4006.5 790.36 4135.85 542.999 4242.07 542.999 4639"
const VIEWBOX_WIDTH = 1459
const VIEWBOX_HEIGHT = 4694
const INITIAL_PATH_LENGTH = 6785.34814453125
const points: TimelinePoint[] = [
{
percentage: 0.0001,
label: "BORN",
subtitle: "C-Section",
details: ["60 biomarker blood test", "60 biomarker blood test"],
variant: "right",
mobileSubtitle: "C-Section",
mobileDetails: ["Microbiome optimization", "Genetic test"],
},
{
percentage: 0.12,
label: "Age 22",
subtitle: "Annual Superpower Baselin",
details: ["60 biomarker blood test", "60 biomarker blood test"],
variant: "right",
mobileSubtitle: "Longevity protocol",
mobileDetails: [
"Prevent Alzheimer’s, heart disease, cancer",
"Prevent Alzheimer’s, heart disease, cancer",
],
},
{
percentage: 0.25,
label: "Age 26",
subtitle: "Gut health protocol",
details: ["60 biomarker blood test", "60 biomarker blood test"],
variant: "left",
mobileSubtitle: "Longevity protocol",
mobileDetails: [
"Prevent Alzheimer’s, heart disease, cancer",
"Prevent Alzheimer’s, heart disease, cancer",
],
},
{
percentage: 0.38,
label: "Age 31",
subtitle: "Fertility protocol",
details: ["60 biomarker blood test", "60 biomarker blood test"],
variant: "left",
mobileSubtitle: "Longevity protocol",
mobileDetails: ["Prevent Alzheimer’s, heart disease, cancer", "Genetic test"],
},
{
percentage: 0.48,
label: "Age 41",
subtitle: "Hormone optimization protocol",
details: ["60 biomarker blood test", "60 biomarker blood test"],
variant: "left",
mobileLabel: "AGE 41",
mobileSubtitle: "Longevity protocol",
mobileDetails: [
"Prevent Alzheimer’s, heart disease, cancer",
"Prevent Alzheimer’s, heart disease, cancer",
],
},
{
percentage: 0.63,
label: "Age 45",
subtitle: "Disease prevention program",
details: ["60 biomarker blood test", "60 biomarker blood test"],
variant: "right",
mobileSubtitle: "Longevity protocol",
mobileDetails: [
"Prevent Alzheimer’s, heart disease, cancer",
"Prevent Alzheimer’s, heart disease, cancer",
],
},
{
percentage: 0.73,
label: "Age 60",
subtitle: "Longevity protocol",
details: ["60 biomarker blood test", "60 biomarker blood test"],
variant: "right",
mobileSubtitle: "Longevity protocol",
mobileDetails: [
"Prevent Alzheimer’s, heart disease, cancer",
"Prevent Alzheimer’s, heart disease, cancer",
],
},
{
percentage: 0.9,
label: "Age 120",
subtitle: "Aging goal",
details: ["60 biomarker blood test", "60 biomarker blood test"],
variant: "left",
mobileSubtitle: "Longevity protocol",
mobileDetails: [
"Prevent Alzheimer’s, heart disease, cancer",
"Prevent Alzheimer’s, heart disease, cancer",
],
},
]
function clamp(value: number, min = 0, max = 1) {
return Math.min(Math.max(value, min), max)
}
function getActiveIndex(progress: number) {
const nextIndex = points.findIndex((point) => progress < point.percentage)
return nextIndex === -1 ? points.length - 1 : Math.max(nextIndex - 1, 0)
}
function LifeGridIcon({ className }: { className?: string }) {
return (
)
}
function TimelineNode({
point,
setRef,
}: {
point: TimelinePoint
setRef: (node: HTMLDivElement | null) => void
}) {
const alignLeft = point.variant === "left"
return (
{alignLeft ? (
{point.label}
) : (
)}
{alignLeft ? (
) : (
{point.label}
)}
)
}
function ExpandedTimelineNode({
point,
setRef,
}: {
point: TimelinePoint
setRef: (node: HTMLDivElement | null) => void
}) {
const alignLeft = point.variant === "left"
return (
{alignLeft ? (
{point.label}
{point.subtitle}
{point.details.map((detail, detailIndex) => (
{detail}
))}
) : (
)}
{alignLeft ? (
) : (
{point.label}
{point.subtitle}
{point.details.map((detail, detailIndex) => (
{detail}
))}
)}
)
}
function MobilePoint({
point,
setRef,
}: {
point: TimelinePoint
setRef: (node: HTMLDivElement | null) => void
}) {
return (
{point.mobileLabel ?? point.label}
{point.mobileSubtitle}
{point.mobileDetails.map((detail, detailIndex) => (
{detail}
))}
)
}
export function Timeline01({ className = "" }: Timeline01Props) {
const trackRef = useRef(null)
const stageRef = useRef(null)
const expandedStageRef = useRef(null)
const svgWrapperRef = useRef(null)
const pathRef = useRef(null)
const progressPathRefs = useRef>([])
const nodeRefs = useRef>([])
const expandedNodeRefs = useRef>([])
const mobilePanelRef = useRef(null)
const mobilePointRefs = useRef>([])
useLayoutEffect(() => {
const track = trackRef.current
const stage = stageRef.current
const expandedStage = expandedStageRef.current
const svgWrapper = svgWrapperRef.current
const path = pathRef.current
if (!track || !stage || !expandedStage || !svgWrapper || !path) return
let frame = 0
let pathLength = INITIAL_PATH_LENGTH
let positions: PointPosition[] = []
let layoutWidth = 0
let isMobile = false
let lastActiveIndex = -1
const syncActivePoint = (activeIndex: number) => {
if (activeIndex === lastActiveIndex) return
nodeRefs.current.forEach((node, index) => {
if (!node) return
const isActive = index === activeIndex
const defaultLabel = node.querySelector("[data-timeline-default]")
const dot = node.querySelector("[data-timeline-dot]")
node.dataset.active = String(isActive)
node.style.zIndex = isActive ? "40" : "20"
if (defaultLabel) {
defaultLabel.style.opacity = isActive ? "0" : "1"
}
if (dot) {
dot.style.boxShadow = isActive
? "0 0 42px rgba(255, 133, 0, 0.95)"
: "0 0 20px rgba(255, 133, 0, 0.7)"
}
})
expandedNodeRefs.current.forEach((node, index) => {
if (!node) return
const isActive = index === activeIndex
const expanded = node.querySelector("[data-timeline-expanded]")
node.dataset.active = String(isActive)
node.style.zIndex = isActive ? "40" : "20"
if (expanded) {
expanded.style.opacity = isActive ? "1" : "0"
expanded.style.transform = isActive ? "translateY(0)" : "translateY(-0.5rem)"
}
})
mobilePointRefs.current.forEach((node, index) => {
if (!node) return
const isActive = index === activeIndex
node.dataset.active = String(isActive)
node.style.opacity = isActive ? "1" : "0"
})
lastActiveIndex = activeIndex
}
const update = () => {
frame = 0
const rect = track.getBoundingClientRect()
const progress = clamp((window.innerHeight - rect.top) / Math.max(rect.height, 1))
const scale = isMobile ? 1 : Math.max(1 - progress, 0.5)
const scaleY = isMobile ? 0.9 : 0.4
const svgScale = layoutWidth / VIEWBOX_WIDTH
const currentPoint = path.getPointAtLength(pathLength * progress)
const verticalShift = currentPoint.y * svgScale * scaleY * scale * 0.5
const mobilePanelVisible = isMobile && rect.bottom > 0 && rect.top < window.innerHeight
stage.style.transform = `translate3d(0, -${verticalShift.toFixed(3)}px, 0)`
expandedStage.style.transform = `translate3d(0, -${verticalShift.toFixed(3)}px, 0)`
svgWrapper.style.transform = `translate3d(0, 0, 1px) scale(${scale.toFixed(5)}) scaleY(${scaleY})`
if (mobilePanelRef.current) {
mobilePanelRef.current.style.opacity = mobilePanelVisible ? "1" : "0"
}
const dashOffset = (pathLength * (1 - progress)).toFixed(3)
progressPathRefs.current.forEach((progressPath) => {
progressPath?.setAttribute("stroke-dashoffset", dashOffset)
})
positions.forEach((position, index) => {
const node = nodeRefs.current[index]
if (!node) return
const left = (position.x * svgScale - layoutWidth / 2) * scale + layoutWidth / 2
const top = position.y * svgScale * scaleY * scale
node.style.left = `${left.toFixed(3)}px`
node.style.top = `${top.toFixed(3)}px`
const expandedNode = expandedNodeRefs.current[index]
if (expandedNode) {
expandedNode.style.left = `${left.toFixed(3)}px`
expandedNode.style.top = `${top.toFixed(3)}px`
}
})
syncActivePoint(getActiveIndex(progress))
}
const requestUpdate = () => {
if (frame) return
frame = window.requestAnimationFrame(update)
}
const measure = () => {
layoutWidth = stage.clientWidth
isMobile = layoutWidth < 810
pathLength = path.getTotalLength()
positions = points.map((point) => path.getPointAtLength(pathLength * point.percentage))
const dashArray = `${pathLength} ${pathLength}`
progressPathRefs.current.forEach((progressPath) => {
progressPath?.setAttribute("stroke-dasharray", dashArray)
})
update()
}
measure()
window.addEventListener("scroll", requestUpdate, { passive: true })
window.addEventListener("resize", measure)
return () => {
if (frame) window.cancelAnimationFrame(frame)
window.removeEventListener("scroll", requestUpdate)
window.removeEventListener("resize", measure)
}
}, [])
return (
Unlock all the potential
your life holds
Unlock all the potential your life holds
{points.map((point, index) => (
{
nodeRefs.current[index] = node
}}
/>
))}
{points.map((point, index) => (
{
expandedNodeRefs.current[index] = node
}}
/>
))}
{points.map((point, index) => (
{
mobilePointRefs.current[index] = node
}}
/>
))}
)
}
export default Timeline01