---
title: "Timeline Histogram"
description: "The portal's delivery timeline: stacked status volume with a drag-to-select scrubber, scale and frame tabs, and jump controls."
component: true
---

```tsx
"use client"

import { useMemo, useState } from "react"
import {
  TIMELINE_SCALE_OPTIONS,
  TimelineHistogram,
  useTimelineScale,
  type TimelineRange,
  type TimelineSeries,
  type TimelineSlot,
} from "@/components/timeline-histogram"

const HOUR_MS = 60 * 60 * 1000

const SERIES: TimelineSeries[] = [
  { key: "2xx", label: "2xx", color: "var(--status-success)", statuses: ["2xx"] },
  { key: "3xx", label: "3xx", color: "var(--status-info)", statuses: ["3xx"] },
  { key: "4xx", label: "4xx", color: "var(--status-warning)", statuses: ["4xx"] },
  { key: "5xx", label: "5xx", color: "var(--status-error)", statuses: ["5xx"] },
]

/** Deterministic delivery volume so the preview never flickers between renders. */
function buildSlots(from: Date, to: Date, intervalSecs: number): TimelineSlot[] {
  const step = intervalSecs * 1000
  const slots: TimelineSlot[] = []
  for (let t = from.getTime(), i = 0; t < to.getTime(); t += step, i++) {
    const base = 40 + ((i * 37) % 90)
    const spike = i % 17 === 0 ? 60 : 0
    const counts = {
      "2xx": base + spike,
      "3xx": (i * 5) % 7,
      "4xx": i % 11 === 3 ? 6 + (i % 5) : i % 3,
      "5xx": i % 13 === 5 ? 9 + (i % 6) : 0,
    }
    slots.push({
      timestamp: new Date(t),
      total: Object.values(counts).reduce((sum, value) => sum + value, 0),
      counts,
    })
  }
  return slots
}

/** Start with a pinned range so the scrubber (glass overlay + handles) is visible. */
const initialSelection = (): TimelineRange => {
  const to = new Date()
  return { from: new Date(to.getTime() - 8 * HOUR_MS), to: new Date(to.getTime() - 2 * HOUR_MS) }
}

export function TimelineHistogramDemo() {
  const [selection, setSelection] = useState<TimelineRange | null>(initialSelection)

  const {
    scale,
    options,
    activeOptionKey,
    selectSpan,
    frameOptions,
    activeFrameKey,
    selectFrame,
    jump,
    canJumpBack,
    canJumpForward,
  } = useTimelineScale({
    selection,
    liveDateWindow: "today",
    planWindowDays: 90,
  })

  // One bucket per ~120 columns of the visible frame, like the portal.
  const intervalSecs = useMemo(
    () => Math.max(60, Math.round((scale.to.getTime() - scale.from.getTime()) / 120 / 1000)),
    [scale.from, scale.to]
  )

  const slots = useMemo(
    () => buildSlots(scale.from, scale.to, intervalSecs),
    [scale.from, scale.to, intervalSecs]
  )

  return (
    <TimelineHistogram
      slots={slots}
      intervalSecs={intervalSecs}
      dateFrom={scale.from}
      dateTo={scale.to}
      series={SERIES}
      isLoading={false}
      selection={selection}
      onApplyRange={setSelection}
      scaleOptions={options ?? TIMELINE_SCALE_OPTIONS}
      activeOptionKey={activeOptionKey}
      onSelectSpan={selectSpan}
      frameOptions={frameOptions}
      activeFrameKey={activeFrameKey}
      onSelectFrame={selectFrame}
      onJump={jump}
      canJumpBack={canJumpBack}
      canJumpForward={canJumpForward}
    />
  )
}

```

This is the timeline from the Repost portal, not a plain chart. Drag across the
plot to select a range, drag the handles to resize it, use the frame tabs to
zoom the visible window, and the chevrons to walk backwards and forwards through
history. The selection is reported through `onApplyRange` so your feed can filter
to it.

## Installation

<ItemInstall name="timeline-histogram" target="components/timeline-histogram/timeline-histogram.tsx" />

## Usage

`useTimelineScale` owns the visible window (scale + frame + jumping); the
component owns selection. Feed it histogram slots, from
`useLogsHistogram` ([@repost/portal-react](/docs/ui/portal-react))
or any source with the same shape.

<ComponentSource name="timeline-histogram-demo" title="components/timeline-histogram-demo.tsx" />

## Props

| Prop | Type | Description |
| ---- | ---- | ----------- |
| `slots` | `TimelineSlot[]` | Time-bucketed counts keyed by series. |
| `intervalSecs` | `number` | Bucket width; drives bar geometry and tooltips. |
| `dateFrom` / `dateTo` | `Date` | The visible window. |
| `series` | `TimelineSeries[]` | Stacked series with label, color, and statuses. |
| `selection` | `TimelineRange \| null` | The applied range, drawn as the on-chart selection. |
| `onApplyRange` | `(range) => void` | Fired when a drag or bucket click commits a range. |
| `scaleOptions` / `activeOptionKey` / `onSelectSpan` | — | The scale rail (Last hour, Today, This week, This month). |
| `frameOptions` / `activeFrameKey` / `onSelectFrame` | — | The zoom rail (24h, 7D, 30D, 90D). |
| `onJump` / `canJumpBack` / `canJumpForward` | — | Walk the window backwards and forwards. |
| `dateRange` | `TimelineDateRangeControl` | Optional date-range picker rendered in the header. |
| `isLoading` | `boolean` | Renders the skeleton state. |
