diff --git a/web/package.json b/web/package.json
index feceac6..b1e83d3 100644
--- a/web/package.json
+++ b/web/package.json
@@ -55,7 +55,7 @@
"lowlight": "^3.3.0",
"lru-cache": "^11.1.0",
"lucide-react": "^0.487.0",
- "motion": "^12.6.5",
+ "motion": "^12.7.4",
"nanoid": "^5.1.5",
"next": "^15.2.3",
"next-themes": "^0.4.6",
diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml
index a8d9798..e9878c4 100644
--- a/web/pnpm-lock.yaml
+++ b/web/pnpm-lock.yaml
@@ -123,7 +123,7 @@ importers:
specifier: ^0.487.0
version: 0.487.0(react@19.1.0)
motion:
- specifier: ^12.6.5
+ specifier: ^12.7.4
version: 12.7.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
nanoid:
specifier: ^5.1.5
diff --git a/web/src/app/chat/components/site-header.tsx b/web/src/app/chat/components/site-header.tsx
index abfd123..311a326 100644
--- a/web/src/app/chat/components/site-header.tsx
+++ b/web/src/app/chat/components/site-header.tsx
@@ -4,6 +4,7 @@
import { StarFilledIcon, GitHubLogoIcon } from "@radix-ui/react-icons";
import Link from "next/link";
+import { NumberTicker } from "~/components/magicui/number-ticker";
import { Button } from "~/components/ui/button";
import { env } from "~/env";
@@ -72,9 +73,9 @@ export async function StarCounter() {
return (
<>
-
- {stars !== null ? stars.toLocaleString() : "—"}
-
+ {stars && (
+
+ )}
>
);
}
diff --git a/web/src/components/magicui/number-ticker.tsx b/web/src/components/magicui/number-ticker.tsx
new file mode 100644
index 0000000..f041565
--- /dev/null
+++ b/web/src/components/magicui/number-ticker.tsx
@@ -0,0 +1,67 @@
+"use client";
+
+import { useInView, useMotionValue, useSpring } from "motion/react";
+import { ComponentPropsWithoutRef, useEffect, useRef } from "react";
+
+import { cn } from "~/lib/utils";
+
+interface NumberTickerProps extends ComponentPropsWithoutRef<"span"> {
+ value: number;
+ startValue?: number;
+ direction?: "up" | "down";
+ delay?: number;
+ decimalPlaces?: number;
+}
+
+export function NumberTicker({
+ value,
+ startValue = 0,
+ direction = "up",
+ delay = 0,
+ className,
+ decimalPlaces = 0,
+ ...props
+}: NumberTickerProps) {
+ const ref = useRef(null);
+ const motionValue = useMotionValue(direction === "down" ? value : startValue);
+ const springValue = useSpring(motionValue, {
+ damping: 60,
+ stiffness: 100,
+ });
+ const isInView = useInView(ref, { once: true, margin: "0px" });
+
+ useEffect(() => {
+ if (isInView) {
+ const timer = setTimeout(() => {
+ motionValue.set(direction === "down" ? startValue : value);
+ }, delay * 1000);
+ return () => clearTimeout(timer);
+ }
+ }, [motionValue, isInView, delay, value, direction, startValue]);
+
+ useEffect(
+ () =>
+ springValue.on("change", (latest) => {
+ if (ref.current) {
+ ref.current.textContent = Intl.NumberFormat("en-US", {
+ minimumFractionDigits: decimalPlaces,
+ maximumFractionDigits: decimalPlaces,
+ }).format(Number(latest.toFixed(decimalPlaces)));
+ }
+ }),
+ [springValue, decimalPlaces],
+ );
+
+ return (
+
+ {startValue}
+
+ );
+}