aboutsummaryrefslogtreecommitdiff
path: root/src/components/UI/Slider.tsx
blob: 22a65ee19ecc4ab00bf1ae8c3657a7594ec52b61 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import { useRef, useCallback, useEffect, useState } from 'react'

interface SliderProps {
  value: number
  min: number
  max: number
  step?: number
  onChange: (value: number) => void
}

export default function Slider({ value, min, max, step = 1, onChange }: SliderProps) {
  const trackRef = useRef<HTMLDivElement>(null)
  const [dragging, setDragging] = useState(false)

  const pct = ((value - min) / (max - min)) * 100

  const updateFromClient = useCallback((clientX: number) => {
    const rect = trackRef.current?.getBoundingClientRect()
    if (!rect) return
    const raw = (clientX - rect.left) / rect.width
    const clamped = Math.max(0, Math.min(1, raw))
    const stepped = Math.round((min + clamped * (max - min)) / step) * step
    onChange(Math.max(min, Math.min(max, stepped)))
  }, [min, max, step, onChange])

  const handleMouseDown = useCallback((e: React.MouseEvent) => {
    e.preventDefault()
    setDragging(true)
    updateFromClient(e.clientX)
  }, [updateFromClient])

  useEffect(() => {
    if (!dragging) return
    const handleMove = (e: MouseEvent) => { e.preventDefault(); updateFromClient(e.clientX) }
    const handleUp = () => setDragging(false)
    window.addEventListener('mousemove', handleMove)
    window.addEventListener('mouseup', handleUp)
    return () => { window.removeEventListener('mousemove', handleMove); window.removeEventListener('mouseup', handleUp) }
  }, [dragging, updateFromClient])

  return (
    <div
      ref={trackRef}
      className="relative h-4 w-full cursor-pointer flex items-center"
      onMouseDown={handleMouseDown}
      role="slider"
      aria-valuemin={min}
      aria-valuemax={max}
      aria-valuenow={value}
      tabIndex={0}
      onKeyDown={(e) => {
        if (e.key === 'ArrowRight' || e.key === 'ArrowUp') { e.preventDefault(); onChange(Math.min(max, value + step)) }
        if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') { e.preventDefault(); onChange(Math.max(min, value - step)) }
      }}
    >
      <div className="absolute h-[3px] w-full rounded-full" style={{ backgroundColor: 'var(--color-panel-border)' }}>
        <div
          className="absolute h-full rounded-full transition-[width] duration-75"
          style={{ width: `${pct}%`, backgroundColor: 'var(--color-accent)' }}
        />
      </div>
      <div
        className="absolute w-3.5 h-3.5 rounded-full shadow-sm border-2 transition-transform duration-75"
        style={{
          left: `${pct}%`,
          marginLeft: '-7px',
          backgroundColor: dragging ? 'var(--color-accent)' : 'var(--color-panel-bg)',
          borderColor: 'var(--color-accent)',
          transform: dragging ? 'scale(1.15)' : 'scale(1)',
        }}
      />
    </div>
  )
}