import { useEffect, useRef, useState, type ReactNode } from "react"; import { motion, type Transition } from "motion/react"; import { cn } from "@/lib/utils"; import { proietta } from "@/lib/molla"; import { useMotoRidotto } from "@/lib/motion"; export type VoceSottosezione = { id: string; label: string; contenuto: ReactNode; /** Se true, il pannello è a tutta larghezza (niente padding), es. barra filtri Classifica. */ aTuttoLarghezza?: boolean; }; /** Tween breve: evita molle + exit che tengono due pannelli in DOM insieme. */ const transizioneTab: Transition = { type: "tween", duration: 0.16, ease: [0.25, 0.1, 0.25, 1] }; /** * Barra di sottosezioni in un'unica fila e pannello che mostra una sola sezione * alla volta, cambiabile anche con swipe sul contenuto. * * `variante="sottolineatura"`: pillola piena sulla tab attiva (Squadra e Classifica CSI); * su mobile la sola barra tab scorre in orizzontale senza allargare la pagina. * Default `pillole`: tab a larghezza uguale (basata sulla etichetta più lunga); se non * entrano nella viewport la barra scorre in orizzontale (Profilo). Il titolo ripetuto sotto * la barra non viene mai mostrato: l’etichetta è già nella tab. */ export function BarraSottosezioni({ voci, defaultId, variante = "pillole", riempiLarghezza = false, }: { voci: VoceSottosezione[]; defaultId?: string; variante?: "pillole" | "sottolineatura"; /** Tab a larghezza uguale che riempiono la barra (es. Campionato, Squadra). */ riempiLarghezza?: boolean; }) { const [attiva, setAttiva] = useState(defaultId ?? voci[0]?.id ?? ""); const direzione = useRef(0); const tabRefs = useRef>({}); // `useMotoRidotto` copre anche i device deboli (RAM bassa), non solo // `prefers-reduced-motion`: qui disattiva sia lo swipe orizzontale sia il // blur della barra tab, i due costi maggiori all'ingresso in una pagina con // sottosezioni (Squadra, Campionato, Profilo). const ridotto = useMotoRidotto(); const indice = Math.max( 0, voci.findIndex((v) => v.id === attiva), ); const voce = voci[indice] ?? voci[0]; const sottolineatura = variante === "sottolineatura"; useEffect(() => { tabRefs.current[attiva]?.scrollIntoView({ behavior: "smooth", inline: "center", block: "nearest", }); }, [attiva]); function vaiA(nuovo: number) { if (nuovo < 0 || nuovo >= voci.length) return; const target = voci[nuovo]; if (!target || target.id === attiva) return; direzione.current = nuovo > indice ? 1 : -1; setAttiva(target.id); } if (!voce) return null; return (
{/* Solo la barra tab può scrollare in orizzontale: non allarga il layout pagina. */}
{voci.map((v) => { const selezionata = v.id === attiva; return ( ); })}
{ // Uno scroll verticale (anche veloce) ha sempre offset.y ben maggiore di // offset.x: la velocity.x, invece, può essere rumorosa su gesti rapidi e da // sola non basta a distinguere uno swipe reale da un semplice scroll in giù. if (Math.abs(info.offset.x) <= Math.abs(info.offset.y)) return; const arrivo = info.offset.x + proietta(info.velocity.x); if (arrivo < -60) vaiA(indice + 1); else if (arrivo > 60) vaiA(indice - 1); }} initial={ridotto ? { opacity: 0 } : { opacity: 0, x: direzione.current * 20 }} animate={{ opacity: 1, x: 0 }} transition={ridotto ? { duration: 0.1 } : transizioneTab} className={cn("touch-pan-y", voce.aTuttoLarghezza ? "px-0 pt-0 pb-4" : "px-5 py-4")} > {voce.contenuto}
); }