← 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>
  );
}