← back to Ken
kalshi-dash/src/components/RingGauge.jsx
139 lines
import React, { useEffect, useRef, useState } from 'react';
/**
* RingGauge — animated SVG circular progress ring
*
* Props:
* value — number 0-100 (percentage to fill)
* size — px diameter (default: 48)
* strokeWidth — px ring width (default: 4)
* color — CSS color (default: var(--cyan))
* label — optional text below the value
* showValue — whether to show percentage in center (default: true)
*
* Usage:
* <RingGauge value={73} size={56} color="var(--green)" label="Win Rate" />
*/
export default function RingGauge({
value = 0,
size = 48,
strokeWidth = 4,
color = 'var(--cyan)',
label,
showValue = true,
}) {
const [animated, setAnimated] = useState(0);
const rafRef = useRef(null);
const startRef = useRef(null);
const duration = 900; // ms
useEffect(() => {
const target = Math.min(100, Math.max(0, value));
const from = animated;
const animate = (ts) => {
if (!startRef.current) startRef.current = ts;
const elapsed = ts - startRef.current;
const t = Math.min(1, elapsed / duration);
// ease-out cubic
const eased = 1 - Math.pow(1 - t, 3);
setAnimated(from + (target - from) * eased);
if (t < 1) {
rafRef.current = requestAnimationFrame(animate);
}
};
startRef.current = null;
rafRef.current = requestAnimationFrame(animate);
return () => cancelAnimationFrame(rafRef.current);
}, [value]); // eslint-disable-line react-hooks/exhaustive-deps
const radius = (size - strokeWidth) / 2;
const circumference = 2 * Math.PI * radius;
const offset = circumference - (animated / 100) * circumference;
const center = size / 2;
const fontSize = size <= 40 ? size * 0.22 : size * 0.2;
return (
<div
style={{
display: 'inline-flex',
flexDirection: 'column',
alignItems: 'center',
gap: 4,
}}
>
<div style={{ position: 'relative', width: size, height: size }}>
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} style={{ display: 'block' }}>
{/* Track ring */}
<circle
cx={center}
cy={center}
r={radius}
fill="none"
stroke="var(--border)"
strokeWidth={strokeWidth}
/>
{/* Progress ring */}
<circle
cx={center}
cy={center}
r={radius}
fill="none"
stroke={color}
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={offset}
style={{
transformOrigin: `${center}px ${center}px`,
transform: 'rotate(-90deg)',
filter: `drop-shadow(0 0 3px ${color}80)`,
}}
/>
</svg>
{showValue && (
<div
style={{
position: 'absolute',
inset: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<span
style={{
fontFamily: 'var(--font-mono)',
fontWeight: 700,
fontSize: fontSize,
color: color,
lineHeight: 1,
letterSpacing: '-0.03em',
}}
>
{Math.round(animated)}%
</span>
</div>
)}
</div>
{label && (
<span
style={{
fontFamily: 'var(--font-body)',
fontSize: '0.6rem',
fontWeight: 600,
textTransform: 'uppercase',
letterSpacing: '0.08em',
color: 'var(--text-muted)',
}}
>
{label}
</span>
)}
</div>
);
}