feat: initialize IoT Standards Lab website project with MAM skills & onboarding guide
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
/**
|
||||
* Number that ticks up from 0 → `value` when scrolled into view.
|
||||
* Uses requestAnimationFrame only (no animation library).
|
||||
*
|
||||
* CUSTOMIZATION HOOK — CONTENT: value / prefix / suffix.
|
||||
*/
|
||||
export default function Counter({
|
||||
value,
|
||||
duration = 1600,
|
||||
prefix = "",
|
||||
suffix = "",
|
||||
className = "",
|
||||
static: isStatic = false,
|
||||
}: {
|
||||
value: number;
|
||||
duration?: number;
|
||||
prefix?: string;
|
||||
suffix?: string;
|
||||
className?: string;
|
||||
/** Render the value as plain text (no tick animation). Use for years,
|
||||
dates, or any value that should not be perceived as "counting up".
|
||||
See LAYOUT_AUDIT #11. */
|
||||
static?: boolean;
|
||||
}) {
|
||||
const ref = useRef<HTMLSpanElement>(null);
|
||||
const [display, setDisplay] = useState(isStatic ? value : 0);
|
||||
|
||||
useEffect(() => {
|
||||
// When `static` is set, the value is rendered as-is with no observer.
|
||||
if (isStatic) return;
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
|
||||
const prefersReduced = window.matchMedia(
|
||||
"(prefers-reduced-motion: reduce)"
|
||||
).matches;
|
||||
|
||||
// Hoisted to the effect scope so the cleanup below can cancel an
|
||||
// in-flight frame on unmount / dep change (the IO callback's own
|
||||
// return value is never used as a cleanup).
|
||||
let raf = 0;
|
||||
|
||||
const io = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (!entry.isIntersecting) return;
|
||||
io.disconnect();
|
||||
|
||||
if (prefersReduced) {
|
||||
setDisplay(value);
|
||||
return;
|
||||
}
|
||||
|
||||
const start = performance.now();
|
||||
const tick = (now: number) => {
|
||||
const t = Math.min((now - start) / duration, 1);
|
||||
// easeOutExpo
|
||||
const eased = t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
|
||||
setDisplay(Math.round(eased * value));
|
||||
if (t < 1) raf = requestAnimationFrame(tick);
|
||||
};
|
||||
raf = requestAnimationFrame(tick);
|
||||
},
|
||||
{ threshold: 0.4 }
|
||||
);
|
||||
|
||||
io.observe(el);
|
||||
return () => {
|
||||
io.disconnect();
|
||||
cancelAnimationFrame(raf);
|
||||
};
|
||||
}, [value, duration, isStatic]);
|
||||
|
||||
return (
|
||||
<span ref={ref} className={`tabular-nums ${className}`}>
|
||||
{prefix}
|
||||
{display}
|
||||
{suffix}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user