npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@itauros/react-timeline

v1.0.1

Published

Headless, composable React timeline/gantt engine. Owns the data model, layout math, interaction state, and virtualization — you own every pixel.

Readme

@itauros/react-timeline

Headless, composable React timeline / gantt engine. Inspired by TanStack Table — the library owns the data model, layout math, interaction state, and virtualization; you own every pixel.

types deps license

  • 🎯 Headless — zero styled components. It computes; you render.
  • Two-axis virtualization — rows and time columns. Built for 10k+ items.
  • 🧮 Native-Date time engine — no dayjs/Luxon dependency; DST- and calendar-correct.
  • 🧩 Plugin architecture — drag, resize, create, dependencies, critical path, baselines, history, keyboard, export, collaboration, a11y. All opt-in and tree-shakeable.
  • 🧬 Fully generic — your item/group payloads flow through with no as casts.
  • 🪶 Tiny — ESM + CJS, sideEffects: false, one peer dep (React).

Table of contents


Install

pnpm add @itauros/react-timeline
# peer deps
pnpm add react react-dom

Entry points:

| Import | Contents | | --- | --- | | @itauros/react-timeline | useTimeline, render primitives, context, types | | @itauros/react-timeline/plugins | all feature plugins (tree-shakeable) | | @itauros/react-timeline/utils | time-scale + native-Date + layout helpers | | @itauros/react-timeline/a11y | accessibility plugin |


Quick start

import {
  useTimeline, TimelineRoot, TimelineSidebar, TimelineGroupRow,
  TimelineCanvas, TimelineHeader, TimelineGrid, TimelineGridColumn,
  TimelineItems, TimelineItem, TimelineNowMarker,
} from '@itauros/react-timeline';
import { dragPlugin, resizePlugin } from '@itauros/react-timeline/plugins';
import { createTimeScale } from '@itauros/react-timeline/utils';

type Person = { name: string };
type Assignment = { project: string; pct: number; color: string };

function Schedule({ people, assignments, onMove }: Props) {
  const tl = useTimeline<Assignment, Person>({
    items: assignments,                 // TimelineItem<Assignment>[]
    groups: people,                     // TimelineGroup<Person>[]
    timeScale: createTimeScale({ unit: 'day', cellWidth: 44 }),
    rowHeight: 56,
    onItemMove: (item, start, end, groupId) => onMove(item.id, start, end, groupId),
    plugins: [resizePlugin({ snapTo: 'day' }), dragPlugin({ snapTo: 'day' })],
  });

  return (
    <TimelineRoot timeline={tl} style={{ height: 480 }}>
      <TimelineSidebar style={{ width: 200 }}>
        {tl.virtualRows.map((row) => (
          <TimelineGroupRow key={row.group.id} group={row.group}>
            {row.group.data.name}        {/* ✓ typed as Person */}
          </TimelineGroupRow>
        ))}
      </TimelineSidebar>

      <TimelineCanvas>
        <TimelineHeader />               {/* auto-renders all header levels */}

        <TimelineGrid>
          {tl.virtualCols.map((c) => (
            <TimelineGridColumn key={c.id} col={c}
              style={{ background: c.isNonWorking ? '#f4f4f5' : undefined }} />
          ))}
        </TimelineGrid>

        <TimelineItems>
          {tl.virtualRows.flatMap((row) =>
            row.items.map((pi) => (
              <TimelineItem key={pi.item.id} item={pi}
                style={{ background: pi.item.data.color }}>
                {pi.item.data.project}   {/* ✓ typed as Assignment */}
              </TimelineItem>
            )),
          )}
        </TimelineItems>

        <TimelineNowMarker />
      </TimelineCanvas>
    </TimelineRoot>
  );
}

Run the interactive demo from the repo: pnpm demo.


Design

The library has one opinion: what to compute. It never decides what a timeline looks like.

  • It owns the data model (items, groups, dependencies), the layout math (date↔pixel mapping, nested rows, collision lanes), the interaction state (a single state machine so drag/resize/select never conflict), and virtualization (only what's near the viewport is rendered).
  • You own rendering — every element is an unstyled slot you fill with your own markup and styles (Tailwind, CSS modules, inline, anything).
  • Features beyond layout are plugins. Import only what you use.
  • Zero runtime dependencies except React. The time engine is built on the native Date with calendar-correct, DST-tolerant arithmetic.

Core concepts

Data model

Everything is generic over your own payload shapes T (item) and G (group):

type TimelineItem<T = unknown> = {
  id: string;
  groupId: string;          // which group/row it belongs to
  start: Date;
  end: Date;
  data: T;                  // arbitrary, fully-typed payload
};

type TimelineGroup<G = unknown> = {
  id: string;
  parentId?: string;        // enables arbitrary nesting depth
  data: G;
};

type Dependency = {
  id: string;
  fromItemId: string;
  toItemId: string;
  type: 'finish-to-start' | 'start-to-start' | 'finish-to-finish' | 'start-to-finish';
  lag?: number;             // ms offset
  data?: unknown;
};

Groups form a tree via parentId; the row model is a depth-first flatten that skips collapsed subtrees. Items that overlap in time within the same group are automatically stacked into lanes so they never visually collide.

Time scale

The scale is a first-class object, not a hardcoded zoom level. Zoom is just a different cellWidth.

type TimeScale = {
  unit: 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year';
  cellWidth: number;                       // px per unit
  format?: (date: Date) => string;
  headerLevels?: { unit: TimeUnit; format?: (d: Date) => string }[];
  nonWorkingPredicate?: (date: Date) => boolean;   // grey out weekends/holidays
  snapPredicate?: (date: Date) => Date;            // snap-to-grid
};

Build and transform scales with the helpers in /utils:

import { createTimeScale, zoomIn, zoomOut, fitToItems, fitToRange }
  from '@itauros/react-timeline/utils';

const day = createTimeScale({
  unit: 'day', cellWidth: 44,
  headerLevels: [{ unit: 'month' }, { unit: 'week' }, { unit: 'day' }],
});

const zoomed = zoomIn(day, { factor: 1.25 });   // just a bigger cellWidth
const fitted = fitToItems(items, containerWidth); // auto-pick unit + cellWidth

Virtualization

Both axes are virtualized independently. Only rows within the vertical viewport (± overscan) and items/columns within the horizontal viewport (± overscan) are materialized. tl.virtualRows, tl.virtualCols, and row.items already contain exactly what you should render this frame.

Interaction state machine

All pointer interactions are modelled as one discriminated union, exposed as tl.interactionState, so plugins coordinate instead of fighting:

type InteractionState =
  | { type: 'idle' }
  | { type: 'hovering';   itemId: string }
  | { type: 'dragging';   itemId: string; itemIds: string[]; originalRect; currentRect; delta; targetGroupId }
  | { type: 'resizing';   itemId: string; edge: 'left' | 'right'; originalRect; currentRect }
  | { type: 'creating';   groupId: string; start: Date; end: Date }
  | { type: 'connecting'; fromItemId: string; toPosition: Point }
  | { type: 'rubberBand'; start: Point; end: Point; selectedIds: string[] };

Read it in your render to draw live previews (e.g. a "ghost" while creating).


DOM / layout contract

TimelineRoot is a flexbox row: TimelineSidebar (left) and TimelineCanvas (right). The canvas contains the single real scroll container; the header strip and the sidebar are scroll-synced via CSS transforms. This keeps one clean coordinate space — the canvas content origin is (0, 0) — for all layout math and virtualization.

TimelineRoot  (display:flex)
├─ TimelineSidebar          row headers, vertically synced (translateY)
└─ TimelineCanvas
   ├─ TimelineHeader        time axis, horizontally synced (translateX)   ← routed here
   └─ scroll container      the only real scroller
      └─ content (totalWidth × totalHeight)
         ├─ TimelineGrid / TimelineGridColumn
         ├─ TimelineRegions / TimelineRegion
         ├─ TimelineItems / TimelineItem
         ├─ TimelineDependencies / TimelineDependencyArrow
         └─ TimelineNowMarker

TimelineCanvas inspects its children and routes the <TimelineHeader> into the synced header strip (matched by a __tlSlot marker); everything else goes into the scrollable content. All rects from getItemRect, getDateX, etc. are in that content space.


API reference

useTimeline(options)

const tl = useTimeline<TItem, TGroup>(options);

Key options (see full TSDoc in your editor for every field):

| Option | Type | Default | Notes | | --- | --- | --- | --- | | items | TimelineItem<T>[] | — | required | | groups | TimelineGroup<G>[] | — | required | | timeScale | TimeScale | — | required | | dependencies | Dependency[] | [] | | | visibleStart / visibleEnd | Date | derived from items | the modelled canvas range | | onVisibleRangeChange | (start, end) => void | — | fires on scroll/zoom; use for lazy data fetching | | rowHeight | number \| (group) => number | 44 | | | rowPadding | number | 8 | | | groupOrder | (a, b) => number | — | sort sibling groups | | weekStartsOn | 0 \| 1 | 1 (Mon) | week alignment | | headerLevelHeight | number | 28 | px per header row | | selectionMode | 'single' \| 'multi' \| 'range' | 'single' | | | selectedIds / onSelectionChange | controlled selection | uncontrolled | | | collapsedIds / onCollapsedChange | controlled collapse | uncontrolled | | | overscan | number | 4 | extra rows/cols rendered off-screen | | onItemMove / onItemResize / onItemCreate / onDependencyCreate | callbacks fired by plugins | — | apply optimistic updates here | | plugins | TimelinePlugin[] | [] | |

Selection and collapse are controlled if you pass the prop, uncontrolled otherwise (the standard React pattern).

The TimelineInstance

Returned by useTimeline; passed to <TimelineRoot timeline={tl}>.

// layout
getItemRect(id): Rect | null
getGroupRowRect(id): Rect | null
getDateX(date): number
getXDate(x): Date

// virtualized slices to render
virtualRows: VirtualRow[]   // visible rows, each with its visible items
virtualCols: VirtualCol[]   // visible base-unit columns
headerCols: HeaderCol[][]   // one array per header level
totalWidth, totalHeight, headerHeight, viewport

// state
getItemState(id): { rect, lane, isSelected, isDragging, isResizing, isColliding, ...plugin flags }
getGroupState(id): { depth, isCollapsed, childIds, itemIds, utilizationRatio }
interactionState: InteractionState

// prop getters (wire to DOM — the primitives do this for you)
getItemProps(id), getGroupProps(id), getCanvasProps(), getScrollContainerProps()

// actions
scrollToDate(date, align?, behavior?)   scrollToItem(id)   scrollToGroup(id)
setTimeScale(scale)
collapseGroup(id, recursive?)   expandGroup(id, recursive?)   isCollapsed(id)
setSelection(ids)   toggleSelection(id, additive?)
undo()   redo()   canUndo()   canRedo()        // no-ops unless historyPlugin is installed
// + any methods contributed by plugins (exportAs, getCriticalPath, getCollaborators, …)

Render primitives

All are unstyled and accept className / style / standard HTML props.

| Primitive | Renders in | Purpose | | --- | --- | --- | | TimelineRoot | — | provides context + flex shell | | TimelineSidebar | Root | left pane (synced vertically) | | TimelineGroupRow | Sidebar | one row header, positioned at its Y | | TimelineCanvas | Root | main pane; routes the header child | | TimelineHeader | Canvas | auto-renders all header levels (or pass renderCell, or full-control children) | | TimelineHeaderCell | custom header | a single positioned header cell | | TimelineGrid / TimelineGridColumn | Canvas | background grid columns | | TimelineItems / TimelineItem | Canvas | the item bars (also exported as TimelineBar) | | TimelineDependencies / TimelineDependencyArrow | Canvas | SVG dependency arrows (dependencyPath helper for custom paths) | | TimelineNowMarker | Canvas | vertical "now" line | | TimelineRegions / TimelineRegion | Canvas | highlight bands (sprints, phases, frozen periods) |

TimelineItem is both the component and the data-model type (merged), so <TimelineItem item={…}> and TimelineItem<Assignment> both work.


Plugins

import {
  dragPlugin, resizePlugin, createPlugin, selectionPlugin,
  dependencyPlugin, criticalPathPlugin, baselinePlugin,
  historyPlugin, keyboardPlugin, exportPlugin, collaborationPlugin,
} from '@itauros/react-timeline/plugins';
import { a11yPlugin } from '@itauros/react-timeline/a11y';

Pass them to useTimeline({ plugins: [...] }). Order matters — the recommended order is selection → resize → drag → create (resize claims edge grabs, drag the interior, selection runs first so a drag starts from a selection).

| Plugin | What it adds | | --- | --- | | dragPlugin | Move items in time and across groups. Options: snapTo, axis ('x' \| 'y' \| 'both'), constrainToGroup, onDragStart/End. Fires onItemMove. | | resizePlugin | Resize an item by grabbing an edge. Options: snapTo, minDuration, handles, edgeThreshold. Snaps live during the preview. Fires onItemResize. | | createPlugin | Drag on empty canvas to sketch a new item. Options: snapTo, minDuration, onCreate. Requires an actual drag (a click won't create). | | selectionPlugin | Click / shift-click / ctrl-click and optional rubber-band selection. Options: mode, rubberBand, keyboard. | | dependencyPlugin | Alt-drag from one item to another to connect, with cycle prevention. Options: editable, cyclePrevention, onCreateDependency/onDeleteDependency. | | criticalPathPlugin | Computes the CPM longest path; adds isCritical to item state and tl.getCriticalPath(). | | baselinePlugin | Renders a planned snapshot behind live bars; adds baselineRect/baselineDriftPx and tl.getBaselineRect(id). | | historyPlugin | Command-based undo/redo. Drag/resize push inverse commands; enables tl.undo/redo/canUndo/canRedo. | | keyboardPlugin | Arrow move/resize, Delete, Escape, mod+z/y, select-all. Configurable bindings, moveStep, resizeStep. | | exportPlugin | tl.exportAs('json' \| 'csv' \| 'svg' \| 'png' \| 'pdf'). Renders a vector snapshot from the layout. | | collaborationPlugin | Broadcast cursors/selection/moves over any channel (BroadcastChannel, WebSocket…). Adds tl.getCollaborators(). | | a11yPlugin | Per-item aria-labels, a polite live region for drag/resize announcements, reduced-motion signalling. |

Writing a plugin

A plugin is a plain object. Hooks: setup, onCanvasPointerDown / onItemPointerDown / onPointerMove / onPointerUp (return true to consume), onKeyDown, extendItemState (add flags/override the rect), itemProps / canvasProps (merge HTML props), and methods (merged onto the instance).

const hoverPlugin: TimelinePlugin = {
  name: 'hover',
  extendItemState: (id, state, ctx) =>
    ctx.getInteraction().type === 'hovering' ? { isHot: true } : undefined,
};

Utilities (/utils)

Pure functions — safe to import standalone, no React required.

  • Scale: createTimeScale, zoomIn, zoomOut, fitToItems, fitToRange, dateToX, xToDate, resolveScale, totalWidthFor, generateCols, generateHeaderCols.
  • Native-Date engine: startOfUnit, endOfUnit, addUnits, floorUnits, unitsBetween, countUnits, daysBetween, quarterOf, isWeekend, isoWeek, defaultFormat, clone, msPerUnitNominal.
  • Layout: buildGroupMaps, buildRowModel, layoutRowItems, visibleRowRange.

Accessibility (/a11y)

a11yPlugin adds per-item aria-labels, a visually-hidden polite live region that announces drag/resize/create outcomes, and a data-reduced-motion attribute (honoring prefers-reduced-motion). The core already applies role="grid" | "row" | "gridcell" via the prop getters and supports keyboard focus on the canvas.


TypeScript

Types flow through end to end — no casting:

const tl = useTimeline<Assignment, Person>({ items, groups, /* … */ });

tl.virtualRows[0].group.data.firstName;        // ✓ Person
tl.virtualRows[0].items[0].item.data.color;    // ✓ Assignment

Generics: T is your item payload, G your group payload. Plugins are generic too (dragPlugin<Assignment, Person>()), though inference usually suffices.


Performance

Targets: 10,000 items / 500 rows at 60 fps pan & zoom; initial render < 100 ms; optimistic reposition < 16 ms.

  • Both axes virtualized; only near-viewport rows/cols/items render.
  • The full item layout (rect + lane + collision) is memoized and recomputed only when data or scale changes — O(n log n), not per scroll frame.
  • Item rects are positioned absolutely so reflows are cheap; drag/resize update only the active item's rect via extendItemState.

Recipes

Live "ghost" preview while creating — read the interaction state:

const st = tl.interactionState;
{st.type === 'creating' && (() => {
  const row = tl.getGroupRowRect(st.groupId)!;
  const x = tl.getDateX(st.start < st.end ? st.start : st.end);
  const w = Math.abs(tl.getDateX(st.end) - tl.getDateX(st.start));
  return <div style={{ position: 'absolute', left: x, top: row.y, width: w, height: row.height }} />;
})()}

Lazy-load data for the visible window:

useTimeline({
  /* … */
  onVisibleRangeChange: (start, end) => fetchAssignments(start, end), // debounce this
});

Optimistic move with undo (with historyPlugin):

useTimeline({
  onItemMove: (item, start, end, groupId) =>
    setItems(prev => prev.map(i => i.id === item.id ? { ...i, start, end, groupId } : i)),
  plugins: [historyPlugin(), dragPlugin({ snapTo: 'day' })],
});

Development

pnpm install
pnpm build        # tsup → ESM + CJS + d.ts for all entry points
pnpm test         # vitest (date / layout / CPM math)
pnpm typecheck
pnpm demo         # Vite playground at localhost:5180

Contact

For support, licensing, partnerships, security reports, or general questions, contact ITAUROS at [email protected].


License

MIT © ITAUROS