# Realtime Visitors

> A visitor activity chart with a live status indicator and country breakdown.

A shadcn/ui block from the [@studio-blocks](https://studio-blocks.arhamkhnz.com/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/studio-blocks/chart-02).

- Page, with a live preview: https://shadcn.exchange/r/studio-blocks/chart-02
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/studio-blocks/chart-02-light-759f463bab6554a9.png
- Registry item: https://studio-blocks.arhamkhnz.com/r/chart-02.json
- Categories: charts
- Listed: 2026-10-24, source last changed 2026-10-24

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/studio-blocks/chart-02.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: `cn`, `recharts`
- Registry items: `card`, `chart`

## Files

### `src/registry/blocks/charts/chart-02/chart-02.tsx`

```tsx
"use client";

import type { ComponentProps, ReactNode } from "react";
import { cn } from "cn";
import { Bar, BarChart, type BarShapeProps, XAxis, YAxis } from "recharts";

import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
  type ChartConfig,
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
} from "@/components/ui/chart";

// Country flag classes require this stylesheet, which is shipped beside the component in the registry.
import "./flags.css";

export type VisitorActivity = { minute: number; visitors: number };
export type VisitorCountry = { code: string; name: string; visitors: number };
export type Chart02Props = ComponentProps<typeof Card> & {
  title?: string;
  activity?: VisitorActivity[];
  countries?: VisitorCountry[];
  visitorsPerMinute?: number;
  isLive?: boolean;
  action?: ReactNode;
};

// Demo values are static. Supply updated activity and country counts from your own data source.
const defaultActivity = [
  0, 6, 12, 20, 12, 0, 6, 6, 0, 4, 0, 20, 15, 4, 6, 0, 4, 12, 20, 0, 4, 20, 12, 0, 6, 6, 0, 20, 0, 4,
].map((visitors, index) => ({ minute: index + 1, visitors }));
const defaultCountries: VisitorCountry[] = [
  { code: "US", name: "United States", visitors: 14 },
  { code: "GB", name: "United Kingdom", visitors: 4 },
  { code: "CA", name: "Canada", visitors: 3 },
  { code: "IN", name: "India", visitors: 3 },
];

const chartConfig = {
  visitors: { color: "var(--color-blue-500)", label: "Visitors" },
} satisfies ChartConfig;

function VisitorBar({ height, payload, width, x, y }: BarShapeProps) {
  const barHeight = Number(height);
  const barWidth = Number(width);
  const barX = Number(x);
  const barY = Number(y);
  if (![barHeight, barWidth, barX, barY].every(Number.isFinite)) return <g />;

  const visitors = (payload as VisitorActivity | undefined)?.visitors ?? 0;
  const opacity = visitors >= 18 ? 0.95 : 0.4;

  return (
    <g>
      <rect
        x={barX}
        y={barY + barHeight - 2}
        width={barWidth}
        height={2}
        rx={1}
        fill="var(--color-visitors)"
        fillOpacity={visitors === 0 ? 0.2 : opacity}
      />
      {visitors > 0 && barHeight > 4 && (
        <rect
          x={barX}
          y={barY}
          width={barWidth}
          height={barHeight - 4}
          rx={2}
          fill="var(--color-visitors)"
          fillOpacity={opacity}
        />
      )}
    </g>
  );
}

export function Chart02({
  title = "Realtime Visitors",
  activity = defaultActivity,
  countries = defaultCountries,
  visitorsPerMinute,
  isLive = true,
  action,
  ...props
}: Chart02Props) {
  const data = activity.filter((item) => Number.isFinite(item.visitors) && item.visitors >= 0);
  const countryData = countries.filter((item) => Number.isFinite(item.visitors) && item.visitors >= 0);
  const total = visitorsPerMinute ?? countryData.reduce((sum, item) => sum + item.visitors, 0);

  return (
    <Card {...props}>
      <CardHeader>
        <CardTitle className="font-normal">{title}</CardTitle>
        {action && <CardAction>{action}</CardAction>}
      </CardHeader>
      <CardContent className="flex flex-col gap-4">
        <div className="flex flex-wrap items-end justify-between gap-3">
          <div className="flex items-baseline gap-1">
            <span className="text-2xl leading-none tracking-tight tabular-nums">{total.toLocaleString("en-US")}</span>
            <span className="text-sm text-muted-foreground">per minute</span>
          </div>
          <div className="flex items-center gap-2 text-sm text-muted-foreground">
            <span aria-hidden="true" className="relative flex size-2">
              {isLive && (
                <span className="absolute inline-flex size-full rounded-full bg-green-500 opacity-75 motion-safe:animate-ping" />
              )}
              <span
                className={cn(
                  "relative inline-flex size-2 rounded-full",
                  isLive ? "bg-green-500" : "bg-muted-foreground",
                )}
              />
            </span>
            <span>{isLive ? "Live" : "Paused"}</span>
          </div>
        </div>
        {data.length > 0 ? (
          <ChartContainer config={chartConfig} className="h-36 w-full text-blue-500">
            <BarChart
              accessibilityLayer
              data={data}
              margin={{ bottom: 0, left: 0, right: 0, top: 0 }}
              barCategoryGap={3}
            >
              <XAxis dataKey="minute" hide />
              <YAxis hide domain={[0, Math.max(22, ...data.map((item) => item.visitors))]} />
              <ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
              <Bar dataKey="visitors" fill="var(--color-visitors)" shape={VisitorBar} />
            </BarChart>
          </ChartContainer>
        ) : (
          <p className="flex h-36 items-center justify-center text-sm text-muted-foreground">No visitor activity.</p>
        )}
        <div className="grid grid-cols-1 sm:grid-cols-2">
          {countryData.map((country, index) => (
            <div
              key={country.code}
              className={cn(
                "flex min-w-0 items-center gap-3 border-border/50 py-3",
                index % 2 === 0 ? "sm:border-r sm:pr-5" : "sm:pl-5",
                index < countryData.length - 1 && "border-b sm:border-b-0",
                index < countryData.length - 2 && "sm:border-b",
              )}
            >
              <span
                aria-hidden="true"
                className={cn(
                  `flag:${country.code.toUpperCase()}`,
                  "shrink-0 rounded-xs text-lg ring-1 ring-foreground/10",
                )}
              />
              <span className="min-w-0 flex-1 truncate text-sm">{country.name}</span>
              <span className="text-sm tabular-nums">{country.visitors.toLocaleString("en-US")}</span>
            </div>
          ))}
        </div>
      </CardContent>
    </Card>
  );
}
```

### `src/registry/blocks/charts/chart-02/flags.css`

Left out to keep this page a readable size (202,975 characters). It is in the registry item above.
