"use client"; import { useState, type RefObject } from "react"; import { motion, useMotionValueEvent, useReducedMotion, useScroll, useSpring } from "motion/react"; type ScrollProgressProps = { /** "bar" runs across the top, "ring" is a round back-to-top button. */ variant?: "bar" | "ring"; /** Scrolling box to follow. Leave out to use the page. */ container?: RefObject; /** Bar height or ring width in pixels. */ size?: number; /** Colour of the progress. */ accent?: string; /** Read by screen readers. */ label?: string; /** Replaces the default place and look. The bar is fixed to the top of the page, the ring to the bottom right. */ className?: string; }; const FOLLOW = { stiffness: 200, damping: 30, restDelta: 0.001 }; const STROKE = 3; /* ScrollProgress: shows how far through the page you are, as a bar across the top or a ring that takes you back up. */ export function ScrollProgress({ variant = "bar", container, size, accent = "#a78bfa", label = "Reading progress", className }: ScrollProgressProps) { const reduce = useReducedMotion(); const { scrollYProgress } = useScroll(container ? { container } : undefined); const smooth = useSpring(scrollYProgress, FOLLOW); const progress = reduce ? scrollYProgress : smooth; const [percent, setPercent] = useState(0); useMotionValueEvent(scrollYProgress, "change", (v) => setPercent(Math.round(v * 100))); // Inside a scrolling box, fixed would pin it to the window, so it sticks to the box instead. const place = container ? "sticky" : "fixed"; if (variant === "bar") { const height = size ?? 3; return ( ); } const width = size ?? 52; const radius = (width - STROKE) / 2; const toTop = () => (container?.current ?? window).scrollTo({ top: 0, behavior: reduce ? "auto" : "smooth" }); return ( ); }