Retour au catalogue
Features Animated
Features avec animations avancees : compteurs, barres de progression, hover effects.
featurescomplex Both Responsive a11y
boldplayfuldarkuniversalsaasgrid
Theme
"use client";
import React from "react";
import { motion, useInView } from "framer-motion";
import * as LucideIcons from "lucide-react";
interface FeatureItem {
id: string;
title: string;
description: string;
icon?: string;
metric?: string;
progress?: number;
}
interface FeaturesAnimatedProps {
badge?: string;
title?: string;
subtitle?: string;
features: FeatureItem[];
}
function getIcon(name?: string) {
if (!name) return null;
return (LucideIcons as unknown as Record<string, React.ElementType>)[name] || null;
}
function ProgressBar({ value }: { value: number }) {
const ref = React.useRef<HTMLDivElement>(null);
const isInView = useInView(ref, { once: true, margin: "-40px" });
return (
<div
ref={ref}
className="h-1.5 w-full rounded-full overflow-hidden"
style={{ backgroundColor: "var(--color-border)" }}
>
<motion.div
className="h-full rounded-full"
style={{ backgroundColor: "var(--color-accent)" }}
initial={{ width: 0 }}
animate={isInView ? { width: `${value}%` } : { width: 0 }}
transition={{ duration: 1.2, ease: [0.16, 1, 0.3, 1], delay: 0.2 }}
/>
</div>
);
}
export default function FeaturesAnimated({
badge,
title,
subtitle,
features,
}: FeaturesAnimatedProps) {
return (
<section
className="py-[var(--section-padding-y,6rem)]"
style={{ backgroundColor: "var(--color-background-dark)" }}
>
<div className="mx-auto max-w-6xl 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 lg:text-5xl"
style={{ color: "var(--color-foreground-on-dark)" }}
>
{title}
</h2>
)}
{subtitle && (
<p className="mt-4 text-base" style={{ color: "var(--color-foreground-light)" }}>
{subtitle}
</p>
)}
</motion.div>
<div className="mt-16 grid grid-cols-1 gap-6 sm:grid-cols-2">
{features.map((feature, i) => {
const Icon = getIcon(feature.icon);
return (
<motion.div
key={feature.id}
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-60px" }}
transition={{ delay: i * 0.1, duration: 0.5, ease: [0.16, 1, 0.3, 1] }}
className="group rounded-[var(--radius-xl,1.5rem)] border p-6 transition-all duration-300 hover:border-opacity-60"
style={{
borderColor: "var(--color-border-dark)",
backgroundColor: "color-mix(in srgb, var(--color-background-dark) 80%, var(--color-background-card))",
}}
>
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
{Icon && (
<Icon
className="h-5 w-5"
style={{ color: "var(--color-accent)" }}
/>
)}
<div>
<h3
className="text-base font-semibold"
style={{ color: "var(--color-foreground-on-dark)" }}
>
{feature.title}
</h3>
<p
className="text-xs mt-0.5"
style={{ color: "var(--color-foreground-light)" }}
>
{feature.description}
</p>
</div>
</div>
{feature.metric && (
<span
className="text-2xl font-bold tracking-tight"
style={{ color: "var(--color-accent)" }}
>
{feature.metric}
</span>
)}
</div>
{feature.progress !== undefined && (
<div className="mt-5">
<ProgressBar value={feature.progress} />
</div>
)}
</motion.div>
);
})}
</div>
</div>
</section>
);
}