# Statistics 22 - Recovery Rate

> A compact recovery rate card showing a headline percentage with month-over-month change and a segmented bar chart with an animated hover tooltip.

A shadcn/ui block from the [@shadcn-space](https://shadcnspace.com/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/shadcn-space/statistics-22).

- Page, with a live preview: https://shadcn.exchange/r/shadcn-space/statistics-22
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/shadcn-space/statistics-22-light-5809243d908b2ae1.png
- Registry item: https://shadcnspace.com/r/statistics-22.json
- Listed: 2026-10-20, source last changed 2026-10-20

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/shadcn-space/statistics-22.json
```

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

## Dependencies

- npm packages: `lucide-react`
- Registry items: `card`, `chart`

## Files

### `src/components/shadcn-space/blocks/statistics-22/page.tsx`

```tsx
import Statistic from "@/components/shadcn-space/blocks/statistics-22/statistics";

export default function StatisticsPage() {
  return <Statistic />;
}
```

### `src/components/shadcn-space/blocks/statistics-22/statistics.tsx`

```tsx
"use client";

import { useEffect, useRef, useState } from "react";
import { Activity, TrendingUp } from "lucide-react";
import { Bar, BarChart, XAxis, YAxis } from "recharts";
import {
  ChartConfig,
  ChartContainer,
  ChartTooltip,
} from "@/components/ui/chart";
import {
  Card,
  CardContent,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";

const chartData = [
  { month: "Jan", rate: 3 },
  { month: "Feb", rate: 4 },
  { month: "Mar", rate: 3 },
  { month: "Apr", rate: 5 },
  { month: "May", rate: 4 },
  { month: "Jun", rate: 6 },
  { month: "Jul", rate: 5 },
  { month: "Aug", rate: 6 },
  { month: "Sep", rate: 5 },
  { month: "Oct", rate: 7 },
  { month: "Nov", rate: 6 },
  { month: "Dec", rate: 8 },
];

const chartConfig = {
  rate: { label: "Recovery", color: "var(--color-primary)" },
} satisfies ChartConfig;

type SegmentedBarProps = {
  x?: number;
  y?: number;
  width?: number;
  height?: number;
  index?: number;
  value?: number;
  activeIndex?: number;
};

function SegmentedBar({
  x = 0,
  y = 0,
  width = 0,
  height = 0,
  index,
  value,
  activeIndex,
}: SegmentedBarProps) {
  if (!height || !value) return null;
  const blockPitch = height / value;
  const blockH = blockPitch * 0.5;
  const isActive = index === activeIndex;
  return (
    <g
      opacity={isActive ? 1 : 0.25}
      className="transition-opacity duration-200"
    >
      {Array.from({ length: value }).map((_, i) => (
        <rect
          key={i}
          x={x}
          y={y + height - (i + 1) * blockPitch + blockPitch * 0.25}
          width={width}
          height={blockH}
          fill="var(--color-primary)"
          rx={1}
        />
      ))}
    </g>
  );
}

function useAnimatedNumber(target: number, duration = 450) {
  const [display, setDisplay] = useState(0);
  const currentRef = useRef(0);
  const rafRef = useRef<number>(0);

  useEffect(() => {
    cancelAnimationFrame(rafRef.current);
    const from = currentRef.current;
    const diff = target - from;
    if (diff === 0) return;
    const startTime = performance.now();
    const tick = (now: number) => {
      const progress = Math.min((now - startTime) / duration, 1);
      const eased = 1 - Math.pow(1 - progress, 3);
      const next = Math.round(from + diff * eased);
      currentRef.current = next;
      setDisplay(next);
      if (progress < 1) {
        rafRef.current = requestAnimationFrame(tick);
      } else {
        currentRef.current = target;
        setDisplay(target);
      }
    };
    rafRef.current = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(rafRef.current);
  }, [target, duration]);

  return display;
}

function AnimatedValue({ value }: { value: number }) {
  const animated = useAnimatedNumber(value);
  return <>{animated.toLocaleString()}</>;
}

type TooltipItem = {
  name?: string;
  value?: number;
  color?: string;
  dataKey?: string;
  type?: string;
};
type ConfigEntry = { label?: string; color?: string };

function AnimatedTooltipContent({
  active,
  payload,
  label,
  config,
}: {
  active?: boolean;
  payload?: TooltipItem[];
  label?: string;
  config?: Record<string, ConfigEntry>;
}) {
  if (!active || !payload?.length) return null;
  const items = payload.filter(
    (item) => item.type !== "none" && item.value !== undefined,
  );
  if (!items.length) return null;
  return (
    <div className="grid min-w-32 items-start gap-1.5 rounded-lg border border-border bg-background px-2.5 py-1.5 text-xs shadow-xl">
      {label && <div className="font-medium">{label}</div>}
      <div className="grid gap-1.5">
        {items.map((item, index) => (
          <div key={index} className="flex w-full items-center gap-2">
            <div
              className="size-2.5 shrink-0 rounded-xs"
              style={{
                backgroundColor:
                  item.color ?? config?.[item.dataKey ?? ""]?.color,
              }}
            />
            <span className="text-muted-foreground">
              {config?.[item.dataKey ?? ""]?.label ?? item.name}
            </span>
            <span className="ml-auto font-mono font-medium tabular-nums text-foreground">
              <AnimatedValue value={item.value as number} />
            </span>
          </div>
        ))}
      </div>
    </div>
  );
}

export default function Statistic() {
  const [currentMonth, setCurrentMonth] = useState(-1);
  const [hoverIndex, setHoverIndex] = useState<number | null>(null);
  const activeIndex = hoverIndex ?? currentMonth;

  useEffect(() => {
    setCurrentMonth(new Date().getMonth());
  }, []);

  return (
    <div className="lg:py-16 sm:py-10 py-8 px-4">
      <Card className="mx-auto flex w-full max-w-sm flex-col gap-6 overflow-visible! py-5 shadow-none">
        <CardHeader className="flex flex-row items-start justify-between gap-2">
          <div className="flex flex-col gap-0.5">
            <CardTitle>Recovery Rate</CardTitle>
            <p className="text-xs text-muted-foreground">Last 12 months</p>
          </div>
          <div className="flex size-8 shrink-0 items-center justify-center rounded-lg border border-border bg-background">
            <Activity size={14} className="text-foreground" aria-hidden="true" />
          </div>
        </CardHeader>
        <CardContent className="flex items-end justify-between gap-6 overflow-visible!">
          <div className="flex flex-col gap-2">
            <h3 className="text-4xl font-semibold leading-none tracking-tight tabular-nums">
              +94%
            </h3>
            <div className="flex items-center gap-2 text-xs">
              <span className="inline-flex items-center gap-1 rounded-full bg-teal-400/10 px-2 py-0.5 font-medium text-teal-400">
                <TrendingUp size={12} />
                +1.8%
              </span>
              <span className="text-muted-foreground">vs last month</span>
            </div>
          </div>
          <ChartContainer
            config={chartConfig}
            className="h-20! w-36! shrink-0 [&_.recharts-wrapper]:overflow-visible!"
          >
            <BarChart
              data={chartData}
              barSize={6}
              barCategoryGap="20%"
              margin={{ top: 0, right: 0, bottom: 0, left: 0 }}
              onMouseMove={(state) => {
                const i = Number(state?.activeTooltipIndex);
                if (!Number.isNaN(i)) setHoverIndex(i);
              }}
              onMouseLeave={() => setHoverIndex(null)}
            >
              <XAxis dataKey="month" hide />
              <YAxis hide domain={[0, 8]} />
              <ChartTooltip
                cursor={false}
                allowEscapeViewBox={{ x: true, y: true }}
                content={<AnimatedTooltipContent config={chartConfig} />}
              />
              <Bar
                dataKey="rate"
                shape={(props: unknown) => (
                  <SegmentedBar
                    {...(props as SegmentedBarProps)}
                    activeIndex={activeIndex}
                  />
                )}
                isAnimationActive
                animationBegin={0}
                animationDuration={600}
                animationEasing="ease-out"
              />
            </BarChart>
          </ChartContainer>
        </CardContent>
      </Card>
    </div>
  );
}
```
