# Odometer: Indian Grouping

> A rupee fundraiser that keeps ticking up. Switch between the Indian system of lakhs and crores (₹2,45,67,890) and international thousands (₹24,567,890): the commas roll to their new places while every digit stays put.

A shadcn/ui block from the [@keyframeui](https://keyframeui.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/keyframeui/odometer-grouping-demo).

- Page, with a live preview: https://shadcn.exchange/r/keyframeui/odometer-grouping-demo
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/keyframeui/odometer-grouping-demo-light-2b2664e582f6357b.png
- Registry item: https://keyframeui.com/r/odometer-grouping-demo.json
- Categories: number, data, motion
- Listed: 2026-10-14, source last changed 2026-10-15

## Install

```bash
npx shadcn@latest add https://keyframeui.com/r/odometer-grouping-demo.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `https://keyframeui.com/r/odometer.json`

## Notes from the registry

Live preview, examples and props: https://keyframeui.com/components/odometer#odometer-grouping-demo

## Files

### `registry/demos/odometer-grouping-demo.tsx`

```tsx
"use client";

import * as React from "react";
import { motion, useInView } from "motion/react";

import { cn } from "@/lib/utils";
import { Odometer } from "@/components/ui/odometer";

type Grouping = "indian" | "international";

const inr: Intl.NumberFormatOptions = { style: "currency", currency: "INR", maximumFractionDigits: 0 };
const GOAL = 5_00_00_000; // ₹5 crore

const systems: { id: Grouping; label: string; sample: string }[] = [
  { id: "indian", label: "Indian", sample: "12,34,567" },
  { id: "international", label: "International", sample: "1,234,567" },
];

// Gifts keep rolling in while the card is on screen. Switching the system
// only moves the commas: no digit turns.
export function OdometerGroupingDemo() {
  const ref = React.useRef<HTMLDivElement>(null);
  const live = useInView(ref, { amount: 0.4 });
  const [grouping, setGrouping] = React.useState<Grouping>("indian");
  const [raised, setRaised] = React.useState(2_45_67_890);
  const [donors, setDonors] = React.useState(18_342);

  React.useEffect(() => {
    if (!live) return;
    let timer = 0;
    const tick = () => {
      // Mostly small gifts, now and then a big one.
      const gift = Math.random() < 0.15 ? 25_000 + Math.random() * 2_00_000 : 500 + Math.random() * 9_500;
      setRaised((r) => Math.min(GOAL, r + Math.round(gift / 10) * 10));
      setDonors((d) => d + 1);
      timer = window.setTimeout(tick, 1400 + Math.random() * 1200);
    };
    timer = window.setTimeout(tick, 1200);
    return () => window.clearTimeout(timer);
  }, [live]);

  const index = systems.findIndex((s) => s.id === grouping);

  return (
    <div ref={ref} className="flex w-full max-w-md flex-col items-center">
      <div className="w-full rounded-2xl border border-zinc-200 bg-white p-6 shadow-sm shadow-zinc-950/[0.03] sm:p-7 dark:border-zinc-800 dark:bg-zinc-900 dark:shadow-none">
        <div className="flex items-center justify-between gap-4">
          <p className="text-sm text-zinc-500 dark:text-zinc-400">Raised for village libraries</p>
          <span className="inline-flex items-center gap-1.5 text-xs text-zinc-500 dark:text-zinc-400">
            <span className="relative flex size-1.5">
              {live ? (
                <span className="absolute inline-flex size-full animate-ping rounded-full bg-emerald-500 opacity-60" />
              ) : null}
              <span className="relative inline-flex size-1.5 rounded-full bg-emerald-500" />
            </span>
            Live
          </span>
        </div>

        <p className="mt-3 text-[clamp(2.25rem,9vw,3.25rem)] leading-none font-semibold tracking-[-0.04em] text-zinc-950 dark:text-zinc-50">
          <Odometer value={raised} format={inr} grouping={grouping} />
        </p>

        <div className="mt-6 h-1.5 overflow-hidden rounded-full bg-zinc-100 dark:bg-zinc-800">
          <motion.div
            className="h-full origin-left rounded-full bg-emerald-500"
            initial={false}
            animate={{ scaleX: raised / GOAL }}
            transition={{ type: "spring", stiffness: 120, damping: 24 }}
          />
        </div>
        <div className="mt-3 flex flex-wrap justify-between gap-x-4 gap-y-1 text-sm text-zinc-500 dark:text-zinc-400">
          <span>
            <Odometer
              value={donors}
              grouping={grouping}
              className="font-medium text-zinc-950 dark:text-zinc-50"
            />{" "}
            donors
          </span>
          <span>
            of{" "}
            <Odometer
              value={GOAL}
              format={inr}
              grouping={grouping}
              className="font-medium text-zinc-950 dark:text-zinc-50"
            />{" "}
            goal
          </span>
        </div>
      </div>

      <div
        role="radiogroup"
        aria-label="Comma grouping"
        className="relative mt-5 grid grid-cols-2 rounded-full border border-zinc-200 bg-zinc-100 p-1 dark:border-zinc-800 dark:bg-zinc-900"
      >
        <motion.span
          aria-hidden
          className="absolute inset-y-1 left-1 w-[calc(50%-0.25rem)] rounded-full bg-white shadow-sm ring-1 shadow-zinc-950/10 ring-zinc-950/5 dark:bg-zinc-700 dark:ring-white/10"
          initial={false}
          animate={{ x: `${index * 100}%` }}
          transition={{ type: "spring", stiffness: 520, damping: 42 }}
        />
        {systems.map((system) => (
          <button
            key={system.id}
            type="button"
            role="radio"
            aria-checked={system.id === grouping}
            onClick={() => setGrouping(system.id)}
            className={cn(
              "relative flex items-baseline justify-center gap-2 rounded-full px-4 py-1.5 text-sm font-medium whitespace-nowrap transition-colors duration-200 outline-none focus-visible:ring-2 focus-visible:ring-zinc-400",
              system.id === grouping
                ? "text-zinc-950 dark:text-zinc-50"
                : "text-zinc-500 hover:text-zinc-800 dark:text-zinc-400 dark:hover:text-zinc-200",
            )}
          >
            {system.label}
            <span className="font-mono text-xs font-normal opacity-60">{system.sample}</span>
          </button>
        ))}
      </div>
    </div>
  );
}
```
