Retour au catalogue
Values Animated Counters
Valeurs illustrees par des compteurs animes et barres de progression.
valuescomplex Both Responsive a11y
bolddarkuniversalsaasgrid
Theme
"use client";
import React, { useRef } from "react";
import { motion, useInView } from "framer-motion";
import * as LucideIcons from "lucide-react";
interface ValueItem { id: string; title: string; metric: string; progress: number; icon?: string; }
interface ValuesAnimatedCountersProps { badge?: string; title?: string; values: ValueItem[]; }
function getIcon(name?: string) { if (!name) return null; return (LucideIcons as unknown as Record<string, React.ElementType>)[name] || null; }
function ProgressRing({ value }: { value: number }) {
const ref = useRef<SVGSVGElement>(null);
const isInView = useInView(ref, { once: true, margin: "-40px" });
const circumference = 2 * Math.PI * 40;
const offset = circumference - (value / 100) * circumference;
return (
<svg ref={ref} width="100" height="100" viewBox="0 0 100 100" className="rotate-[-90deg]">
<circle cx="50" cy="50" r="40" fill="none" strokeWidth="6" style={{ stroke: "var(--color-border)" }} />
<motion.circle cx="50" cy="50" r="40" fill="none" strokeWidth="6" strokeLinecap="round" style={{ stroke: "var(--color-accent)" }} strokeDasharray={circumference} initial={{ strokeDashoffset: circumference }} animate={isInView ? { strokeDashoffset: offset } : { strokeDashoffset: circumference }} transition={{ duration: 1.5, ease: [0.16, 1, 0.3, 1], delay: 0.2 }} />
</svg>
);
}
export default function ValuesAnimatedCounters({ badge, title, values }: ValuesAnimatedCountersProps) {
return (
<section className="py-[var(--section-padding-y,6rem)]" style={{ backgroundColor: "var(--color-background-dark)" }}>
<div className="mx-auto max-w-5xl px-[var(--container-padding-x,1.5rem)]">
<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-80px" }} transition={{ duration: 0.5 }} className="text-center max-w-2xl mx-auto">
{badge && <span className="inline-block text-xs font-medium tracking-wider uppercase px-3 py-1 rounded-full border" style={{ color: "var(--color-accent)", borderColor: "var(--color-border-dark)" }}>{badge}</span>}
{title && <h2 className="mt-4 text-3xl font-bold tracking-tight md:text-4xl" style={{ color: "var(--color-foreground-on-dark)" }}>{title}</h2>}
</motion.div>
<div className="mt-14 grid grid-cols-2 gap-8 md:grid-cols-4">
{values.map((value, i) => {
const Icon = getIcon(value.icon);
return (
<motion.div key={value.id} initial={{ opacity: 0, y: 24 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-40px" }} transition={{ delay: i * 0.1, duration: 0.5 }} className="flex flex-col items-center text-center">
<div className="relative">
<ProgressRing value={value.progress} />
<div className="absolute inset-0 flex items-center justify-center">
{Icon && <Icon className="h-6 w-6" style={{ color: "var(--color-accent)" }} />}
</div>
</div>
<span className="mt-3 text-2xl font-bold" style={{ color: "var(--color-accent)" }}>{value.metric}</span>
<span className="mt-1 text-xs" style={{ color: "var(--color-foreground-light)" }}>{value.title}</span>
</motion.div>
);
})}
</div>
</div>
</section>
);
}