1"use client";
2
3import React, { useEffect, useRef, useState } from "react";
4
5interface ProgressProps {
6 value: number;
7 max?: number;
8 label?: string;
9 showValue?: boolean;
10 className?: string;
11}
12
13export function Progress({
14 value,
15 max = 100,
16 label,
17 showValue = false,
18 className = "",
19}: ProgressProps) {
20 const pct = Math.min(Math.max((value / max) * 100, 0), 100);
21 const [displayed, setDisplayed] = useState(0);
22 const t = useRef<ReturnType<typeof setTimeout> | null>(null);
23
24 useEffect(() => {
25 t.current = setTimeout(() => setDisplayed(pct), 80);
26 return () => { if (t.current) clearTimeout(t.current); };
27 }, [pct]);
28
29 return (
30 <div className={["flex flex-col gap-2", className].join(" ")}>
31 {(label || showValue) && (
32 <div className="flex items-center justify-between gap-4">
33 {label && (
34 <span className="text-[13px] font-medium text-neutral-700 truncate">{label}</span>
35 )}
36 {showValue && (
37 <span aria-hidden className="text-[12px] tabular-nums text-neutral-500 shrink-0">
38 {Math.round(pct)}%
39 </span>
40 )}
41 </div>
42 )}
43 <div
44 className="w-full h-2 rounded-full p-px overflow-hidden"
45 role="progressbar"
46 aria-label={label}
47 aria-valuenow={value}
48 aria-valuemin={0}
49 aria-valuemax={max}
50 style={{ background: "var(--well)", boxShadow: "var(--well-shadow)" }}
51 >
52 <div
53 className="h-full rounded-full relative overflow-hidden"
54 style={{
55 width: `${displayed}%`,
56 /* Never collapses below a pill, so 1% still reads as started */
57 minWidth: displayed > 0 ? 6 : 0,
58 background: "var(--face-dark)",
59 boxShadow: "inset 0 2px 3px -2px rgba(255,255,255,0.135), 0 0 2px rgba(0,0,0,0.2)",
60 transition: "width 700ms var(--ease-out-soft)",
61 }}
62 />
63 </div>
64 </div>
65 );
66}