# RealtimeCounter

> A live active-users card - a large count, a per-minute mini chart and ranked lists such as top pages and countries.

Source: https://docs.nasaqui.com/components/realtime-counter

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/realtime-counter.json
```

RealtimeCounter shows how many people are on the site right now. The count is announced politely to screen readers, a bar chart shows users per minute, and `sections` list the top active pages or countries. The host polls and passes new values; the component only presents them.

## When to use

- The "Right now" card of an analytics page.
- Live counts with a short history.

## When not to use

- Daily totals: use [`MetricTiles`](https://docs.nasaqui.com/components/metric-tiles).
- Log streams: use [`LogViewer`](https://docs.nasaqui.com/components/log-viewer).

## Import

```tsx
import { RealtimeCounter } from "@fadymondy/nasaq/web";
```

## Quick start

```tsx
import { RealtimeCounter } from "@fadymondy/nasaq/web";

export function Now() {
  return (
    <RealtimeCounter
      value={87}
      perMinute={[62, 70, 66, 81, 79, 87]}
      sections={[{ id: "pages", title: "Top active pages", ltr: true, rows: [{ id: "a", label: "/pricing", value: 14 }] }]}
    />
  );
}
```

## Anatomy

```
RealtimeCounter        data-slot="realtime-counter"  (a Card)
  live dot + count     aria-live="polite" region
  MiniBar              users per minute
  sections             ranked rows
```

## API

### RealtimeCounter

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `number` | required | Active users now. |
| `perMinute` | `readonly number[]` | none | Users per minute, oldest first. |
| `sections` | `readonly RealtimeSection[]` | none | `{ id, title, rows: { id, label, value }[], ltr? }`. |
| `updatedAt` | `number \| Date \| string` | none | When the data was read; shown as a relative time. |
| `live` | `boolean` | `true` | Shows the pulsing dot; set false when polling is paused. |
| `title / description` | `ReactNode` | none | Card header. |
| `className` | `string` | none | Extra classes on the root. |
| `labels` | `Partial<RealtimeCounterLabels>` | none | Replace any built-in English or Arabic string. |

## Examples

Polling from the host:

```tsx
import { RealtimeCounter } from "@fadymondy/nasaq/web";
import { useEffect, useState } from "react";

export function Live({ read }: { read: () => Promise<number> }) {
  const [n, setN] = useState(0);
  useEffect(() => {
    const id = setInterval(() => void read().then(setN), 5000);
    return () => clearInterval(id);
  }, [read]);
  return <RealtimeCounter value={n} />;
}
```

Paused:

```tsx
import { RealtimeCounter } from "@fadymondy/nasaq/web";

export const Paused = () => <RealtimeCounter value={0} live={false} />;
```

## Accessibility

The count is in a polite live region, so changes are announced without interrupting. The pulsing dot is decorative and stops under `prefers-reduced-motion`. The mini chart has a text summary.

## RTL & i18n

- Sections align to the inline start; the mini chart runs right to left.
- Set `ltr` on a section of paths.
- Every figure uses the active locale with Western digits and is bidi-isolated (`Num`), so `+12.4%` keeps its order in Arabic.

## Styling & tokens

- Colours come from tokens (`--primary`, `--nq-success`, `--nq-warning`, `--nq-danger`, `--nq-tag-*`); never pass raw hex.
- Live dot uses `--nq-success`.

## Do / Don't

- Do throttle polling to every few seconds.
- Don't animate the count digit by digit.

## Related

- [`stat-card`](https://docs.nasaqui.com/components/stat-card)
- [`chart`](https://docs.nasaqui.com/components/chart)
- [`google-analytics-page`](https://docs.nasaqui.com/components/google-analytics-page)

## Lab

https://docs.nasaqui.com/?path=/docs/components-analytics-realtime-counter--docs

## Code

### React

```tsx
import { RealtimeCounter } from "@fadymondy/nasaq/web";

export function Now() {
  return (
    <RealtimeCounter
      value={87}
      perMinute={[62, 70, 66, 81, 79, 87]}
      sections={[{ id: "pages", title: "Top active pages", ltr: true, rows: [{ id: "a", label: "/pricing", value: 14 }] }]}
    />
  );
}
```

### shadcn

```tsx
import { RealtimeCounter } from "@/components/ui/realtime-counter";

export function Now() {
  return (
    <RealtimeCounter
      value={87}
      perMinute={[62, 70, 66, 81, 79, 87]}
      sections={[{ id: "pages", title: "Top active pages", ltr: true, rows: [{ id: "a", label: "/pricing", value: 14 }] }]}
    />
  );
}
```

### Vue

```vue
<script setup lang="ts">
import { NqRealtimeCounter } from "@fadymondy/nasaq/vue";
</script>

<template>
  <NqRealtimeCounter
    :value="87"
    :per-minute="[62, 70, 66, 81, 79, 87]"
    :sections="[{ id: 'pages', title: 'Top active pages', ltr: true, rows: [{ id: 'a', label: '/pricing', value: 14 }] }]"
  />
</template>
```

### Blade

```blade
<x-nq::realtime-counter
    :value="87"
    :per-minute="[62, 70, 66, 81, 79, 87]"
    :sections="[['id' => 'pages', 'title' => 'Top active pages', 'ltr' => true, 'rows' => [['id' => 'a', 'label' => '/pricing', 'value' => 14]]]]"
/>
```

### HTML + Alpine

```html
<div data-slot="realtime-counter" data-live="true"
    class="flex flex-col gap-4 rounded-card border border-border bg-card py-4 text-card-foreground">
    <div data-slot="card-header" class="grid auto-rows-min items-start gap-1 px-4 has-data-[slot=card-action]:grid-cols-[1fr_auto]"><h3 data-slot="card-title" class="text-label text-foreground"><span class="inline-flex items-center gap-2">
                <span aria-hidden="true" class="relative flex size-2.5">
                    <span class="absolute inline-flex size-full rounded-full bg-nq-success opacity-60 motion-safe:animate-ping"></span>                    <span class="relative inline-flex size-2.5 rounded-full bg-nq-success"></span>
                </span>
                Right now
                <span class="text-caption font-normal text-muted-foreground">Live</span>
            </span></h3>
        <div data-slot="card-description" class="text-body-sm text-muted-foreground">People active in the last 30 minutes</div></div>
    <div data-slot="card-content" class="px-4 flex flex-col gap-4"><div class="flex flex-wrap items-end justify-between gap-4">
            <div role="status" aria-label="87 active users" class="text-display text-foreground tabular-nums" data-slot="realtime-value">
                <bdi data-slot="num" data-numeric="" class="tabular-nums">87</bdi>
            </div>
                            <div data-slot="mini-bar"  role="img" aria-label="Users per minute, last 30 minutes"  class="shrink-0 h-12 w-40">
    <div class="flex size-full items-end gap-0.5 pt-0.5">
                    <span class="min-w-0 flex-1 rounded-[2px]"
                style="height: 71.2644%; background-color: var(--primary); opacity: 0.35"></span>
                    <span class="min-w-0 flex-1 rounded-[2px]"
                style="height: 80.4598%; background-color: var(--primary); opacity: 0.35"></span>
                    <span class="min-w-0 flex-1 rounded-[2px]"
                style="height: 75.8621%; background-color: var(--primary); opacity: 0.35"></span>
                    <span class="min-w-0 flex-1 rounded-[2px]"
                style="height: 93.1034%; background-color: var(--primary); opacity: 0.35"></span>
                    <span class="min-w-0 flex-1 rounded-[2px]"
                style="height: 90.8046%; background-color: var(--primary); opacity: 0.35"></span>
                    <span class="min-w-0 flex-1 rounded-[2px]"
                style="height: 100%; background-color: var(--primary); opacity: 1"></span>
            </div>
</div>
                    </div>
                                    <div class="grid gap-4 sm:grid-cols-2">
                                    <section aria-label="Top active pages" class="flex flex-col gap-2">
                        <h4 class="text-label text-muted-foreground">Top active pages</h4>
                        <ul class="flex flex-col gap-1.5">
                                                            <li class="flex items-center justify-between gap-3 text-body-sm">
                                                                            <bdi dir="ltr" class="min-w-0 truncate text-foreground">/pricing</bdi>
                                                                        <bdi data-slot="num" data-numeric="" class="tabular-nums text-muted-foreground">14</bdi>
                                </li>
                                                    </ul>
                    </section>
                            </div></div>
</div>
```
