Add immediate opt-out for FadeIn and AnimatedNumber #3

Merged
GROK merged 1 commits from fix/motion-immediate into main 2026-08-20 17:18:49 -05:00
3 changed files with 17 additions and 5 deletions
Showing only changes of commit 1db797b4db - Show all commits
+6 -2
View File
@@ -8,10 +8,13 @@ export function AnimatedNumber({
value, value,
format = defaultFormat, format = defaultFormat,
className, className,
immediate = false,
}: { }: {
value: number; value: number;
format?: (n: number) => string; format?: (n: number) => string;
className?: string; className?: string;
/** Count up on mount instead of waiting to scroll into view. Dashboards. */
immediate?: boolean;
}) { }) {
const ref = React.useRef<HTMLSpanElement>(null); const ref = React.useRef<HTMLSpanElement>(null);
const inView = useInView(ref, { once: true, margin: "-20% 0px" }); const inView = useInView(ref, { once: true, margin: "-20% 0px" });
@@ -19,11 +22,12 @@ export function AnimatedNumber({
const mv = useMotionValue(0); const mv = useMotionValue(0);
const spring = useSpring(mv, { stiffness: 90, damping: 20 }); const spring = useSpring(mv, { stiffness: 90, damping: 20 });
const [display, setDisplay] = React.useState(format(0)); const [display, setDisplay] = React.useState(format(0));
const active = immediate || inView;
React.useEffect(() => { React.useEffect(() => {
if (reduce) { setDisplay(format(value)); return; } if (reduce) { setDisplay(format(value)); return; }
if (inView) mv.set(value); if (active) mv.set(value);
}, [inView, value, reduce, mv, format]); }, [active, value, reduce, mv, format]);
React.useEffect(() => spring.on("change", (v) => setDisplay(format(v))), [spring, format]); React.useEffect(() => spring.on("change", (v) => setDisplay(format(v))), [spring, format]);
+4 -1
View File
@@ -24,6 +24,8 @@ export interface StatCardProps {
intent?: StatIntent; intent?: StatIntent;
icon?: React.ReactNode; icon?: React.ReactNode;
className?: string; className?: string;
/** Count up on mount. Pass through to AnimatedNumber. */
immediate?: boolean;
} }
/** Dashboard metric tile. Pass `animate` for a count-up, or `value` for static. */ /** Dashboard metric tile. Pass `animate` for a count-up, or `value` for static. */
@@ -36,6 +38,7 @@ export function StatCard({
intent = "default", intent = "default",
icon, icon,
className, className,
immediate,
}: StatCardProps) { }: StatCardProps) {
return ( return (
<div className={cn("rounded-lg border border-border bg-surface p-4 shadow-sm", className)}> <div className={cn("rounded-lg border border-border bg-surface p-4 shadow-sm", className)}>
@@ -44,7 +47,7 @@ export function StatCard({
{icon && <span className="text-muted [&_svg]:size-4">{icon}</span>} {icon && <span className="text-muted [&_svg]:size-4">{icon}</span>}
</div> </div>
<p className={cn("mt-1 font-display text-2xl font-bold", INTENT[intent])}> <p className={cn("mt-1 font-display text-2xl font-bold", INTENT[intent])}>
{animate != null ? <AnimatedNumber value={animate} format={format} /> : value} {animate != null ? <AnimatedNumber value={animate} format={format} immediate={immediate} /> : value}
</p> </p>
{sub && <p className="mt-0.5 text-xs text-muted">{sub}</p>} {sub && <p className="mt-0.5 text-xs text-muted">{sub}</p>}
</div> </div>
+7 -2
View File
@@ -9,19 +9,24 @@ export function FadeIn({
delay = 0, delay = 0,
y = 8, y = 8,
className, className,
immediate = false,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
delay?: number; delay?: number;
y?: number; y?: number;
className?: string; className?: string;
/** Animate on mount instead of waiting to scroll into view. Dashboards. */
immediate?: boolean;
}) { }) {
const reduce = useReducedMotion(); const reduce = useReducedMotion();
const visible = { opacity: 1, y: 0 };
return ( return (
<motion.div <motion.div
className={className} className={className}
initial={reduce ? false : { opacity: 0, y }} initial={reduce ? false : { opacity: 0, y }}
whileInView={{ opacity: 1, y: 0 }} {...(immediate
viewport={{ once: true, margin: "-10% 0px" }} ? { animate: visible }
: { whileInView: visible, viewport: { once: true, margin: "-10% 0px" as const } })}
transition={reduce ? { duration: 0 } : { duration: 0.4, ease: easeOut, delay }} transition={reduce ? { duration: 0 } : { duration: 0.4, ease: easeOut, delay }}
> >
{children} {children}