Files
CRAPP/src/components/motion/Numero.tsx
T
davideandClaude Opus 5 e7863ab950 Passa le animazioni a molle interrompibili
Il movimento era fatto con `@keyframes` e transizioni a durata fissa: nessuna
di quelle animazioni può essere afferrata a metà. Se l'utente scorre o tocca
mentre una è in corso, quella va avanti per conto suo e per ripartire deve
prima finire.

`lib/molla.ts` raccoglie i parametri, che sono i due di Apple in «Designing
Fluid Interfaces» — rimbalzo e durata, non massa/rigidità/smorzamento.
`molla.ui` (nessun sorpasso) è il default; `molla.slancio` si usa solo dopo un
gesto che portava già inerzia. `proietta()` calcola dove finirebbe un elemento
lanciato, con la stessa decelerazione esponenziale dello scroll iOS.

Nei tre componenti:
- `Reveal` compare quando entra davvero nel viewport. Prima partiva al mount,
  quindi gli elementi sotto la piega consumavano l'animazione a vuoto e
  l'utente li trovava già fermi. Il ritardo massimo scende da 480 a 200 ms.
- `Barra` anima `scaleX` invece di `width`: la larghezza rifà il layout a ogni
  frame, la trasformazione la gestisce il compositore.
- `Numero` usa una molla al posto dell'interpolazione a durata fissa: se il
  dato cambia a metà conteggio il numero cambia rotta da dov'è invece di
  ripartire da capo. Sparisce la prop `durata`, che nessuno passava.

`lib/motion.ts` resta solo per il rilevamento del movimento ridotto (che qui
guarda anche `deviceMemory`, cosa che `useReducedMotion` non fa) e per i
coriandoli.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 12:42:01 +02:00

33 lines
1.0 KiB
TypeScript

import { useEffect } from "react";
import { motion, useMotionValue, useSpring, useTransform, useReducedMotion } from "motion/react";
import { molla } from "@/lib/molla";
/**
* Contatore animato per statistiche e percentuali.
*
* Usa una molla invece di un'interpolazione a durata fissa: se il valore
* cambia mentre il conteggio è in corso, il numero cambia rotta dal punto in
* cui si trova invece di ripartire da capo.
*/
export function Numero({ valore, suffisso = "" }: { valore: number; suffisso?: string }) {
const ridotto = useReducedMotion();
const grezzo = useMotionValue(0);
const morbido = useSpring(grezzo, molla.ui);
const testo = useTransform(morbido, (n) => `${Math.round(n)}${suffisso}`);
useEffect(() => {
if (ridotto) {
grezzo.jump(valore);
morbido.jump(valore);
return;
}
grezzo.set(valore);
}, [valore, ridotto, grezzo, morbido]);
return (
<motion.span className="tabular-nums" aria-label={`${valore}${suffisso}`}>
{testo}
</motion.span>
);
}