'use client'; import { Pause, Play, SkipBack, SkipForward } from 'lucide-react'; import { useCallback, useEffect, useId, useRef, useState } from 'react'; import { t } from '@/i18n'; import { cn } from '@/lib/cn'; /** * Time machine control: a year slider over an explicit list of available years (gaps allowed), with * play / pause (steps every `interval` ms, loops back to the first year), first / last jumps, keyboard * arrows on the range input, a large tap-friendly thumb and a compact year readout. Fully controlled: * the parent owns `year` (usually mirrored in the URL via `useUrlState`). */ export function YearSlider({ years, year, onChange, playable = true, interval = 700, compact = false, className, label, autoplay = false, onPlayingChange, ticks = true, }: { years: number[]; year: number; onChange: (year: number) => void; playable?: boolean; interval?: number; compact?: boolean; className?: string; label?: string; autoplay?: boolean; onPlayingChange?: (playing: boolean) => void; ticks?: boolean; }) { const id = useId(); const [playing, setPlaying] = useState(autoplay); const idx = Math.max(0, years.indexOf(year)); const timer = useRef | null>(null); const idxRef = useRef(idx); idxRef.current = idx; const stop = useCallback(() => { setPlaying(false); }, []); useEffect(() => { onPlayingChange?.(playing); if (!playing) { if (timer.current) clearInterval(timer.current); timer.current = null; return; } if (years.length < 2) { setPlaying(false); return; } timer.current = setInterval(() => { const next = idxRef.current + 1; if (next >= years.length) { setPlaying(false); return; } onChange(years[next]!); }, interval); return () => { if (timer.current) clearInterval(timer.current); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [playing, interval, years.length]); const first = years[0] ?? year; const last = years[years.length - 1] ?? year; const start = () => { if (idxRef.current >= years.length - 1) onChange(first); setPlaying(true); }; const tickYears = ticks ? pickTicks(years, compact ? 3 : 6) : []; return (
{playable ? (
) : null}
{ stop(); onChange(years[Number(e.target.value)] ?? year); }} aria-label={label ?? t('control.year')} aria-valuetext={String(year)} aria-valuemin={first} aria-valuemax={last} aria-valuenow={year} className="ca-range h-11 w-full min-w-0 md:h-9" style={{ touchAction: 'pan-y' }} disabled={years.length < 2} /> {tickYears.length ? (
{tickYears.map((y) => { const pos = years.length > 1 ? (years.indexOf(y) / (years.length - 1)) * 100 : 0; return ( {y} ); })}
) : null}
{year}
); } /** Evenly spaced tick years including first and last, snapping to round decades when the span allows. */ export function pickTicks(years: number[], n: number): number[] { if (years.length < 2) return years; const first = years[0]!; const last = years[years.length - 1]!; const span = last - first; const step = span / Math.max(1, n - 1); const out = new Set([first, last]); for (let i = 1; i < n - 1; i++) { const target = first + i * step; const decade = Math.round(target / 10) * 10; const candidate = years.includes(decade) ? decade : years.reduce((a, b) => (Math.abs(b - target) < Math.abs(a - target) ? b : a), first); if (candidate - first > step / 2 && last - candidate > step / 2) out.add(candidate); } return Array.from(out).sort((a, b) => a - b); }