{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"lib-motion","type":"registry:lib","title":"motion helpers","description":"Entrance motion shared by dashboards: things on a page arriving one after another. Each class is a full Tailwind","dependencies":[],"registryDependencies":[],"files":[{"path":"registry/dashral/lib/motion.ts","type":"registry:file","target":"components/dashral/lib/motion.ts","content":"import * as React from \"react\"\nimport type { CSSProperties } from \"react\"\n\n// Entrance motion shared by dashboards: things on a page arriving one after another. Each class is a full Tailwind\n// class list; give each item its place with `stagger(i)`. With reduced motion everything is simply there.\n\n/** A row, list item or card rising a little as it fades in. */\nexport const enterMotion =\n  \"animate-in fade-in-0 fill-mode-both duration-(--duration-considered) ease-enter slide-in-from-bottom-1 motion-reduce:animate-none\"\n\n/** A horizontal bar growing in from the start (the left, or the right in Arabic). Its track needs `overflow-hidden`. */\nexport const growMotion =\n  \"animate-in fill-mode-both duration-(--duration-considered) ease-enter slide-in-from-start-full motion-reduce:animate-none\"\n\n/** A vertical bar growing up from the bottom. Its track needs `overflow-hidden`. */\nexport const riseMotion =\n  \"animate-in fill-mode-both duration-(--duration-considered) ease-enter slide-in-from-bottom-full motion-reduce:animate-none\"\n\n/**\n * The delay for the i-th item: `style={stagger(i)}`. Long lists stop adding delay after `max` items, so the last\n * row never waits long.\n */\nexport function stagger(i: number, step = 45, max = 12): CSSProperties {\n  return { animationDelay: `${Math.min(i, max) * step}ms` }\n}\n\n/**\n * Whether to keep still: the OS asks for less motion, or the app turned motion down with data-motion=\"reduce\" on\n * <html>. The same two signals the motion-reduce: and motion-safe: classes follow.\n */\nexport function reducedMotion() {\n  if (typeof window === \"undefined\") return false\n  return document.documentElement.dataset.motion === \"reduce\" || window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n}\n\n/** Whether the page has hydrated: after that, every mount is a client navigation. */\nlet pageHydrated = false\n\n/** A duration token in milliseconds. Browsers may hand it back as \"480ms\" or, minified, \".48s\". */\nexport function cssDuration(name: string, fallback: number) {\n  const raw = getComputedStyle(document.documentElement).getPropertyValue(name).trim()\n  const n = parseFloat(raw)\n  if (!Number.isFinite(n)) return fallback\n  return raw.endsWith(\"ms\") ? n : raw.endsWith(\"s\") ? n * 1000 : n\n}\n\n/**\n * Counts to `value` from the number on screen when it changes (and from 0 on mount if asked); never with reduced\n * motion. Starting from what's shown — not a remembered value — survives React running effects twice in development\n * and a value changing again mid-count. Used by MetricCard; use it for any number that should count.\n */\nexport function useCountTo(value: number, fromZero = false) {\n  const [shown, setShown] = React.useState(value)\n  const onScreen = React.useRef(value)\n\n  // Counting from 0 on mount: set 0 before the browser paints, so there's no flash of the final number. On the\n  // first load the server's number is already on screen; only pull it back to 0 while the page is still arriving.\n  React.useLayoutEffect(() => {\n    if (!fromZero) return\n    const late = !pageHydrated && performance.now() > 900\n    pageHydrated = true\n    if (late || reducedMotion()) return\n    onScreen.current = 0\n    setShown(0)\n    // Only on mount.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [])\n\n  React.useEffect(() => {\n    const start = onScreen.current\n    if (start === value || reducedMotion()) {\n      onScreen.current = value\n      setShown(value)\n      return\n    }\n    // Count in the values' own precision: whole numbers stay whole on the way.\n    const decimals = Math.max(...[start, value].map((n) => (String(n).split(\".\")[1] ?? \"\").length))\n    // As long as the theme's --duration-considered (480ms), with a little more so the eye can follow the count.\n    const duration = cssDuration(\"--duration-considered\", 480) * 1.5\n    const began = performance.now()\n    let frame = requestAnimationFrame(function tick(now) {\n      // The first frame's time can be a hair before `began`: keep progress within 0…1.\n      const t = Math.min(1, Math.max(0, (now - began) / duration))\n      const eased = 1 - (1 - t) ** 3\n      const next = Number((start + (value - start) * eased).toFixed(decimals))\n      onScreen.current = next\n      setShown(next)\n      if (t < 1) frame = requestAnimationFrame(tick)\n    })\n    return () => cancelAnimationFrame(frame)\n  }, [value])\n  return shown\n}\n"}]}