first commit

This commit is contained in:
Puechberty Arthur
2026-03-30 20:19:05 +02:00
commit c96a23dc12
27 changed files with 8889 additions and 0 deletions
+297
View File
@@ -0,0 +1,297 @@
"use client";
import React, { useEffect, useCallback } from "react";
import { useStopwatch } from "../hooks/useStopwatch";
import { formatTime, copyToClipboard } from "../lib/utils";
export default function Stopwatch({
onCopyFeedback,
}: {
onCopyFeedback: () => void;
}) {
const { elapsed, isRunning, laps, toggle, reset, lap } = useStopwatch();
const time = formatTime(elapsed);
// Keyboard shortcuts
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
switch (e.code) {
case "Space":
e.preventDefault();
toggle();
break;
case "KeyR":
if (!e.ctrlKey && !e.metaKey) {
e.preventDefault();
reset();
}
break;
case "KeyL":
e.preventDefault();
lap();
break;
case "KeyC":
if (!e.ctrlKey && !e.metaKey) {
e.preventDefault();
copyToClipboard(time.full).then((ok) => {
if (ok) onCopyFeedback();
});
}
break;
}
},
[toggle, reset, lap, time.full, onCopyFeedback]
);
useEffect(() => {
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [handleKeyDown]);
const lapDiffs = laps.map((lapTime, i) => {
const prev = i === 0 ? 0 : laps[i - 1];
return lapTime - prev;
});
return (
<div className="flex flex-col items-center gap-6 sm:gap-8 animate-fade-in">
{/* Time display */}
<div className="relative group">
<button
onClick={() =>
copyToClipboard(time.full).then((ok) => {
if (ok) onCopyFeedback();
})
}
className="cursor-pointer select-none focus:outline-none"
title="Copier le temps"
aria-label="Copier le temps"
>
<div className="font-mono text-5xl sm:text-7xl md:text-8xl lg:text-9xl font-bold tracking-tight tabular-nums transition-all duration-100">
<span className="text-zinc-500 dark:text-zinc-500">
{time.hours}
</span>
<span className="text-zinc-400 dark:text-zinc-600">:</span>
<span className="text-zinc-900 dark:text-zinc-100">
{time.minutes}
</span>
<span className="text-zinc-400 dark:text-zinc-600">:</span>
<span className="text-zinc-900 dark:text-zinc-100">
{time.seconds}
</span>
<span className="text-zinc-400 dark:text-zinc-600">.</span>
<span className="text-zinc-400 dark:text-zinc-500 text-3xl sm:text-5xl md:text-6xl lg:text-7xl">
{time.milliseconds}
</span>
</div>
</button>
{/* Copy icon hint */}
<div className="absolute -right-8 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-60 transition-opacity">
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
</svg>
</div>
</div>
{/* Controls */}
<div className="flex items-center gap-3 sm:gap-4">
<button
onClick={toggle}
className={`px-6 sm:px-8 py-3 sm:py-4 rounded-2xl font-semibold text-base sm:text-lg transition-all duration-200 active:scale-95 cursor-pointer ${
isRunning
? "bg-amber-500/15 text-amber-600 dark:text-amber-400 hover:bg-amber-500/25 border border-amber-500/30"
: "bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 hover:bg-emerald-500/25 border border-emerald-500/30"
}`}
>
{isRunning ? (
<span className="flex items-center gap-2">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<rect x="6" y="4" width="4" height="16" rx="1" />
<rect x="14" y="4" width="4" height="16" rx="1" />
</svg>
Pause
</span>
) : elapsed > 0 ? (
<span className="flex items-center gap-2">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<polygon points="5 3 19 12 5 21 5 3" />
</svg>
Reprendre
</span>
) : (
<span className="flex items-center gap-2">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<polygon points="5 3 19 12 5 21 5 3" />
</svg>
Démarrer
</span>
)}
</button>
{isRunning && (
<button
onClick={lap}
className="px-5 sm:px-6 py-3 sm:py-4 rounded-2xl font-semibold text-base sm:text-lg bg-sky-500/15 text-sky-600 dark:text-sky-400 hover:bg-sky-500/25 border border-sky-500/30 transition-all duration-200 active:scale-95 animate-scale-in cursor-pointer"
>
<span className="flex items-center gap-2">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z" />
<line x1="4" x2="4" y1="22" y2="15" />
</svg>
Tour
</span>
</button>
)}
{(elapsed > 0 || laps.length > 0) && (
<button
onClick={reset}
className="px-5 sm:px-6 py-3 sm:py-4 rounded-2xl font-semibold text-base sm:text-lg bg-red-500/10 text-red-500 dark:text-red-400 hover:bg-red-500/20 border border-red-500/20 transition-all duration-200 active:scale-95 animate-scale-in cursor-pointer"
>
<span className="flex items-center gap-2">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
<path d="M3 3v5h5" />
</svg>
Reset
</span>
</button>
)}
</div>
{/* Laps */}
{laps.length > 0 && (
<div className="w-full max-w-lg mt-2 animate-fade-in">
<h3 className="text-sm font-medium text-zinc-500 dark:text-zinc-400 mb-3 uppercase tracking-wider">
Tours ({laps.length})
</h3>
<div className="max-h-64 overflow-y-auto rounded-xl border border-zinc-200 dark:border-zinc-800 bg-white/50 dark:bg-zinc-900/50 backdrop-blur-sm">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-zinc-200 dark:border-zinc-800 text-zinc-500 dark:text-zinc-400">
<th className="py-2.5 px-4 text-left font-medium">#</th>
<th className="py-2.5 px-4 text-right font-medium">
Temps total
</th>
<th className="py-2.5 px-4 text-right font-medium">Delta</th>
</tr>
</thead>
<tbody>
{[...laps]
.map((l, i) => ({ lap: l, diff: lapDiffs[i], index: i }))
.reverse()
.map(({ lap: l, diff, index }) => {
const best = Math.min(...lapDiffs);
const worst = Math.max(...lapDiffs);
const isBest = lapDiffs.length > 1 && diff === best;
const isWorst = lapDiffs.length > 1 && diff === worst;
return (
<tr
key={index}
className={`border-b border-zinc-100 dark:border-zinc-800/50 last:border-0 transition-colors ${
isBest
? "bg-emerald-500/5"
: isWorst
? "bg-red-500/5"
: "hover:bg-zinc-50 dark:hover:bg-zinc-800/30"
}`}
>
<td className="py-2.5 px-4 font-medium text-zinc-600 dark:text-zinc-300">
<span className="flex items-center gap-1.5">
{index + 1}
{isBest && (
<span className="text-[10px] bg-emerald-500/20 text-emerald-600 dark:text-emerald-400 px-1.5 py-0.5 rounded-full font-semibold">
BEST
</span>
)}
{isWorst && (
<span className="text-[10px] bg-red-500/20 text-red-500 dark:text-red-400 px-1.5 py-0.5 rounded-full font-semibold">
SLOW
</span>
)}
</span>
</td>
<td className="py-2.5 px-4 text-right font-mono text-zinc-800 dark:text-zinc-200">
{formatTime(l).full}
</td>
<td
className={`py-2.5 px-4 text-right font-mono ${
isBest
? "text-emerald-600 dark:text-emerald-400"
: isWorst
? "text-red-500 dark:text-red-400"
: "text-zinc-500 dark:text-zinc-400"
}`}
>
+{formatTime(diff).full}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
)}
{/* Keyboard shortcuts */}
<div className="flex flex-wrap justify-center gap-x-4 gap-y-1 text-xs text-zinc-400 dark:text-zinc-600 mt-4">
<span>
<kbd className="px-1.5 py-0.5 bg-zinc-200 dark:bg-zinc-800 rounded text-[10px] font-mono">
Espace
</kbd>{" "}
Start/Pause
</span>
<span>
<kbd className="px-1.5 py-0.5 bg-zinc-200 dark:bg-zinc-800 rounded text-[10px] font-mono">
L
</kbd>{" "}
Tour
</span>
<span>
<kbd className="px-1.5 py-0.5 bg-zinc-200 dark:bg-zinc-800 rounded text-[10px] font-mono">
R
</kbd>{" "}
Reset
</span>
<span>
<kbd className="px-1.5 py-0.5 bg-zinc-200 dark:bg-zinc-800 rounded text-[10px] font-mono">
C
</kbd>{" "}
Copier
</span>
</div>
</div>
);
}
+59
View File
@@ -0,0 +1,59 @@
"use client";
import React, { createContext, useContext, useEffect, useState } from "react";
type Theme = "light" | "dark";
interface ThemeContextType {
theme: Theme;
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType>({
theme: "dark",
toggleTheme: () => {},
});
export function useTheme() {
return useContext(ThemeContext);
}
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>("dark");
const [mounted, setMounted] = useState(false);
useEffect(() => {
const stored = localStorage.getItem("chrono-theme") as Theme | null;
if (stored) {
setTheme(stored);
} else if (window.matchMedia("(prefers-color-scheme: light)").matches) {
setTheme("light");
}
setMounted(true);
}, []);
useEffect(() => {
if (!mounted) return;
const root = document.documentElement;
if (theme === "dark") {
root.classList.add("dark");
} else {
root.classList.remove("dark");
}
localStorage.setItem("chrono-theme", theme);
}, [theme, mounted]);
const toggleTheme = () => {
setTheme((prev) => (prev === "dark" ? "light" : "dark"));
};
if (!mounted) {
return <div style={{ visibility: "hidden" }}>{children}</div>;
}
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
+408
View File
@@ -0,0 +1,408 @@
"use client";
import React, { useState, useCallback } from "react";
import { useTimers, TimerItem } from "../hooks/useTimers";
import { formatTime, copyToClipboard } from "../lib/utils";
const PRESETS = [
{ label: "1 min", seconds: 60 },
{ label: "3 min", seconds: 180 },
{ label: "5 min", seconds: 300 },
{ label: "10 min", seconds: 600 },
{ label: "15 min", seconds: 900 },
{ label: "30 min", seconds: 1800 },
];
const RADIUS = 54;
const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
function CircularProgress({
progress,
finished,
}: {
progress: number;
finished: boolean;
}) {
const offset = CIRCUMFERENCE * (1 - Math.max(0, Math.min(1, progress)));
return (
<svg viewBox="0 0 120 120" className="w-full h-full">
{/* Track */}
<circle
cx="60"
cy="60"
r={RADIUS}
fill="none"
strokeWidth="5"
className="stroke-zinc-200 dark:stroke-zinc-800"
/>
{/* Progress */}
<circle
cx="60"
cy="60"
r={RADIUS}
fill="none"
strokeWidth="5"
strokeLinecap="round"
strokeDasharray={CIRCUMFERENCE}
strokeDashoffset={offset}
transform="rotate(-90 60 60)"
className={`transition-[stroke-dashoffset] duration-100 ${
finished
? "stroke-red-500 dark:stroke-red-400"
: progress > 0.25
? "stroke-sky-500 dark:stroke-cyan-400"
: progress > 0.1
? "stroke-amber-500 dark:stroke-amber-400"
: "stroke-red-500 dark:stroke-red-400"
}`}
/>
{/* Glow when close to finish */}
{progress <= 0.1 && progress > 0 && (
<circle
cx="60"
cy="60"
r={RADIUS}
fill="none"
strokeWidth="8"
strokeLinecap="round"
strokeDasharray={CIRCUMFERENCE}
strokeDashoffset={offset}
transform="rotate(-90 60 60)"
className="stroke-red-500/20 dark:stroke-red-400/20 animate-pulse-ring"
/>
)}
</svg>
);
}
function TimerCard({
timer,
onToggle,
onReset,
onDelete,
onCopyFeedback,
}: {
timer: TimerItem;
onToggle: () => void;
onReset: () => void;
onDelete: () => void;
onCopyFeedback: () => void;
}) {
const time = formatTime(timer.remaining);
const progress = timer.duration > 0 ? timer.remaining / timer.duration : 0;
return (
<div
className={`relative rounded-2xl border p-4 sm:p-5 transition-all duration-300 animate-scale-in ${
timer.finished
? "border-red-500/40 bg-red-500/5 dark:bg-red-500/5 shadow-lg shadow-red-500/10"
: "border-zinc-200 dark:border-zinc-800 bg-white/60 dark:bg-zinc-900/60 hover:border-zinc-300 dark:hover:border-zinc-700"
} backdrop-blur-sm`}
>
{/* Delete button */}
<button
onClick={onDelete}
className="absolute top-3 right-3 p-1 rounded-lg text-zinc-400 hover:text-red-500 hover:bg-red-500/10 transition-colors cursor-pointer"
aria-label="Supprimer"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
<div className="flex items-center gap-4 sm:gap-5">
{/* Circular progress */}
<div className="w-20 h-20 sm:w-24 sm:h-24 shrink-0 relative">
<CircularProgress progress={progress} finished={timer.finished} />
{/* Percentage in center */}
<div className="absolute inset-0 flex items-center justify-center">
<span
className={`text-xs sm:text-sm font-semibold ${
timer.finished
? "text-red-500 dark:text-red-400"
: "text-zinc-600 dark:text-zinc-300"
}`}
>
{timer.finished ? "Fini !" : `${Math.round(progress * 100)}%`}
</span>
</div>
</div>
{/* Time + controls */}
<div className="flex-1 min-w-0">
{timer.label && (
<p className="text-sm text-zinc-500 dark:text-zinc-400 mb-1 truncate">
{timer.label}
</p>
)}
<button
onClick={() =>
copyToClipboard(time.full).then((ok) => {
if (ok) onCopyFeedback();
})
}
className="cursor-pointer"
title="Copier le temps"
>
<p
className={`font-mono text-2xl sm:text-3xl font-bold tabular-nums ${
timer.finished
? "text-red-500 dark:text-red-400 animate-pulse"
: "text-zinc-900 dark:text-zinc-100"
}`}
>
{time.hours !== "00" && (
<>
{time.hours}
<span className="text-zinc-400 dark:text-zinc-600">:</span>
</>
)}
{time.minutes}
<span className="text-zinc-400 dark:text-zinc-600">:</span>
{time.seconds}
</p>
</button>
{/* Controls */}
<div className="flex items-center gap-2 mt-2.5">
{!timer.finished && (
<button
onClick={onToggle}
className={`px-3.5 py-1.5 rounded-xl text-sm font-semibold transition-all duration-200 active:scale-95 cursor-pointer ${
timer.isRunning
? "bg-amber-500/15 text-amber-600 dark:text-amber-400 hover:bg-amber-500/25 border border-amber-500/30"
: "bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 hover:bg-emerald-500/25 border border-emerald-500/30"
}`}
>
{timer.isRunning ? "⏸ Pause" : "▶ Start"}
</button>
)}
<button
onClick={onReset}
className="px-3.5 py-1.5 rounded-xl text-sm font-semibold bg-zinc-200/60 dark:bg-zinc-800/60 text-zinc-600 dark:text-zinc-300 hover:bg-zinc-300/60 dark:hover:bg-zinc-700/60 border border-zinc-300/50 dark:border-zinc-700/50 transition-all duration-200 active:scale-95 cursor-pointer"
>
Reset
</button>
</div>
</div>
</div>
</div>
);
}
export default function Timer({
onCopyFeedback,
}: {
onCopyFeedback: () => void;
}) {
const {
timers,
addTimer,
startTimer,
pauseTimer,
resetTimer,
deleteTimer,
toggleTimer,
} = useTimers();
const [hours, setHours] = useState(0);
const [minutes, setMinutes] = useState(5);
const [seconds, setSeconds] = useState(0);
const [label, setLabel] = useState("");
const handleAdd = useCallback(() => {
const totalMs = (hours * 3600 + minutes * 60 + seconds) * 1000;
if (totalMs <= 0) return;
const id = addTimer(totalMs, label || `${hours > 0 ? hours + "h " : ""}${minutes > 0 ? minutes + "m " : ""}${seconds > 0 ? seconds + "s" : ""}`.trim());
startTimer(id);
setLabel("");
}, [hours, minutes, seconds, label, addTimer, startTimer]);
const handlePreset = useCallback(
(totalSeconds: number) => {
const h = Math.floor(totalSeconds / 3600);
const m = Math.floor((totalSeconds % 3600) / 60);
const s = totalSeconds % 60;
setHours(h);
setMinutes(m);
setSeconds(s);
const id = addTimer(totalSeconds * 1000, `${m > 0 ? m + " min" : ""}${s > 0 ? " " + s + "s" : ""}`);
startTimer(id);
},
[addTimer, startTimer]
);
return (
<div className="flex flex-col items-center gap-6 sm:gap-8 animate-fade-in w-full max-w-xl mx-auto">
{/* Timer creation */}
<div className="w-full rounded-2xl border border-zinc-200 dark:border-zinc-800 bg-white/60 dark:bg-zinc-900/60 backdrop-blur-sm p-5 sm:p-6">
{/* Presets */}
<div className="flex flex-wrap justify-center gap-2 mb-5">
{PRESETS.map((p) => (
<button
key={p.seconds}
onClick={() => handlePreset(p.seconds)}
className="px-3.5 py-1.5 rounded-xl text-sm font-medium bg-zinc-100 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 hover:bg-sky-500/15 hover:text-sky-600 dark:hover:text-sky-400 hover:border-sky-500/30 border border-zinc-200 dark:border-zinc-700 transition-all duration-200 active:scale-95 cursor-pointer"
>
{p.label}
</button>
))}
</div>
{/* Time input */}
<div className="flex items-center justify-center gap-1 sm:gap-2 mb-4">
<TimeInput
value={hours}
onChange={setHours}
max={99}
label="Heures"
/>
<span className="text-2xl sm:text-3xl font-bold text-zinc-400 dark:text-zinc-600 mt-5">
:
</span>
<TimeInput
value={minutes}
onChange={setMinutes}
max={59}
label="Minutes"
/>
<span className="text-2xl sm:text-3xl font-bold text-zinc-400 dark:text-zinc-600 mt-5">
:
</span>
<TimeInput
value={seconds}
onChange={setSeconds}
max={59}
label="Secondes"
/>
</div>
{/* Label input */}
<div className="flex items-center gap-2 mb-4">
<input
type="text"
value={label}
onChange={(e) => setLabel(e.target.value)}
placeholder="Nom du minuteur (optionnel)"
className="flex-1 px-3 py-2 rounded-xl border border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-sm text-zinc-800 dark:text-zinc-200 placeholder-zinc-400 dark:placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-sky-500/40 transition-all"
onKeyDown={(e) => {
if (e.key === "Enter") handleAdd();
}}
/>
</div>
{/* Add button */}
<button
onClick={handleAdd}
disabled={hours === 0 && minutes === 0 && seconds === 0}
className="w-full py-3 rounded-2xl font-semibold text-base bg-sky-500/15 text-sky-600 dark:text-sky-400 hover:bg-sky-500/25 border border-sky-500/30 transition-all duration-200 active:scale-[0.98] disabled:opacity-30 disabled:cursor-not-allowed cursor-pointer"
>
<span className="flex items-center justify-center gap-2">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
Ajouter un minuteur
</span>
</button>
</div>
{/* Active timers */}
{timers.length > 0 && (
<div className="w-full space-y-3">
<h3 className="text-sm font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">
Minuteurs actifs ({timers.length})
</h3>
{timers.map((timer) => (
<TimerCard
key={timer.id}
timer={timer}
onToggle={() => toggleTimer(timer.id)}
onReset={() => resetTimer(timer.id)}
onDelete={() => deleteTimer(timer.id)}
onCopyFeedback={onCopyFeedback}
/>
))}
</div>
)}
{/* Empty state */}
{timers.length === 0 && (
<div className="text-center py-8 text-zinc-400 dark:text-zinc-600">
<svg
className="mx-auto mb-3 opacity-40"
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" />
</svg>
<p className="text-sm">
Sélectionnez un preset ou réglez un temps personnalisé
</p>
</div>
)}
</div>
);
}
function TimeInput({
value,
onChange,
max,
label,
}: {
value: number;
onChange: (v: number) => void;
max: number;
label: string;
}) {
const increment = () => onChange(Math.min(max, value + 1));
const decrement = () => onChange(Math.max(0, value - 1));
return (
<div className="flex flex-col items-center">
<button
onClick={increment}
className="p-1 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-200 transition-colors cursor-pointer"
aria-label={`Augmenter ${label}`}
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<polyline points="18 15 12 9 6 15" />
</svg>
</button>
<input
type="number"
value={value}
onChange={(e) => {
const v = parseInt(e.target.value) || 0;
onChange(Math.max(0, Math.min(max, v)));
}}
className="w-16 sm:w-20 text-center text-3xl sm:text-4xl font-mono font-bold bg-zinc-100 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl py-2 text-zinc-900 dark:text-zinc-100 focus:outline-none focus:ring-2 focus:ring-sky-500/40 transition-all"
min={0}
max={max}
/>
<button
onClick={decrement}
className="p-1 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-200 transition-colors cursor-pointer"
aria-label={`Diminuer ${label}`}
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<polyline points="6 9 12 15 18 9" />
</svg>
</button>
<span className="text-[10px] text-zinc-400 dark:text-zinc-500 uppercase tracking-wider mt-0.5">
{label}
</span>
</div>
);
}