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

clockboard

v0.1.0

Published

An array of analog clocks that move together to form patterns and display the time. Canvas-rendered React component, 60fps.

Readme

ClockBoard

A 20×8 grid of analog clocks that animate together to form patterns and display the time. Canvas-rendered React component — 60fps, zero CSS imports.

Install

pnpm add clockboard

Peer dependency: react >=18.

Quick Start

import { ClockBoard } from 'clockboard'

function App() {
  return (
    <div style={{ height: '100vh' }}>
      <ClockBoard />
    </div>
  )
}

The default behavior (clockRandom) cycles through randomized ambient patterns, displaying the current time whenever the minute changes.

The board fills its parent. The clock grid keeps a fixed 20×8 aspect ratio and scales uniformly to fit the padded area — constrained by whichever dimension is tighter.

Props

Behavior

| Prop | Type | Default | Description | |------|------|---------|-------------| | behavior | Behavior | clockRandom() | Dynamic behavior controlling pattern transitions over time | | pattern | GridPattern | — | Static pattern — sets all clocks to a fixed position (disables behavior) | | duration | number | 10000 | Transition duration in ms when using the pattern prop |

When pattern is set, it takes priority over behavior. The board animates to the given pattern once and holds. When pattern is cleared (set to undefined), the behavior resumes.

Appearance

| Prop | Type | Default | Description | |------|------|---------|-------------| | handColor | string | 'rgb(222, 222, 222)' | Color of the clock hands | | faceColor | string | '#141414' | Background color of each clock face | | boardColor | string | '#171717' | Background of the board container — any CSS background value, including gradients | | boardPadding | string | '4%' | Padding around the clock grid | | boardRadius | number | 8 | Border radius of the board in pixels | | clockGap | string | '0.5%' | Gap between individual clocks | | easing | 'ease-in-out' \| 'linear' | 'ease-in-out' | Easing curve for hand transitions | | rotation | 'clockwise' \| 'shortest' | 'clockwise' | Direction hands rotate — 'clockwise' always sweeps forward; 'shortest' takes the shortest arc | | className | string | — | CSS class on the outer container | | style | CSSProperties | — | Inline styles on the outer container |

// Full-viewport board
<ClockBoard style={{ height: '100vh' }} />

// Fixed-size container
<div style={{ width: 800, height: 400 }}>
  <ClockBoard />
</div>

Behaviors

Behaviors control what the board displays over time. They receive an apply callback to push patterns and return a cleanup function.

type Behavior = (apply: ApplyPattern) => () => void
type ApplyPattern = (pattern: GridPattern, duration?: number) => void

Built-in Behaviors

clockRandom

The default. Cycles through random ambient patterns and displays the current system time (24-hour format) whenever the minute changes.

import { ClockBoard, clockRandom } from 'clockboard'

<ClockBoard />
// equivalent to:
<ClockBoard behavior={clockRandom()} />

// Custom options
<ClockBoard behavior={clockRandom({ duration: 3000, hold: 2000, timeDuration: 4000, timeHold: 15000 })} />

Accepts an options object or positional args (patterns?, duration?, hold?, timeDuration?, timeHold?).

| Field | Type | Default | Description | |-------|------|---------|-------------| | patterns | GridPattern[] | ALL_PATTERNS | Pool of ambient patterns | | duration | number | 10000 | Ambient transition duration (ms) | | hold | number | 800 | Pause between ambient transitions (ms) | | timeDuration | number | 5000 | Time display transition duration (ms) | | timeHold | number | 10000 | How long the time stays visible before resuming ambient (ms) |

clockCycle

Same as clockRandom but steps through ambient patterns in order rather than randomly. Same signatures and defaults.

import { ClockBoard, clockCycle } from 'clockboard'

<ClockBoard behavior={clockCycle()} />
<ClockBoard behavior={clockCycle({ hold: 250 })} />

clock

Pure time display. Shows the current system time and updates every minute. No ambient patterns.

import { ClockBoard, clock } from 'clockboard'

<ClockBoard behavior={clock()} />
<ClockBoard behavior={clock({ duration: 2000 })} />

| Field | Type | Default | Description | |-------|------|---------|-------------| | duration | number | 10000 | Transition duration when the time updates (ms) |

random

Continuously transitions through random ambient patterns. No time display.

import { ClockBoard, random } from 'clockboard'

<ClockBoard behavior={random()} />
<ClockBoard behavior={random({ hold: 250 })} />

Accepts an options object or positional args (patterns?, duration?, hold?).

| Field | Type | Default | Description | |-------|------|---------|-------------| | patterns | GridPattern[] | ALL_PATTERNS | Pool of patterns | | duration | number | 10000 | Transition duration (ms) | | hold | number | 800 | Pause after transition before next (ms) |

cycle

Cycles through ambient patterns in order. No time display. Same signatures and defaults as random.

import { ClockBoard, cycle } from 'clockboard'

<ClockBoard behavior={cycle()} />
<ClockBoard behavior={cycle({ duration: 3000 })} />

Custom Behaviors

import { ClockBoard, NOON, RADIAL } from 'clockboard'
import type { Behavior } from 'clockboard'

const alternate: Behavior = (apply) => {
  let flip = false
  const interval = setInterval(() => {
    apply(flip ? NOON : RADIAL, 3000)
    flip = !flip
  }, 8000)
  apply(NOON, 3000)
  return () => clearInterval(interval)
}

<ClockBoard behavior={alternate} />

The apply function accepts any GridPattern and an optional duration in milliseconds.

Patterns

A GridPattern is an 8-row × 20-column 2D array where each cell is a ClockState — a tuple of two angles in degrees ([hand1, hand2]). is 12 o'clock, 90° is 3 o'clock, 180° is 6 o'clock, 270° is 9 o'clock.

Built-in Ambient Patterns

All 13 patterns are individually exported and also available as the ALL_PATTERNS array:

import {
  NOON, RADIAL, CONVERGE, CHECKERBOARD, WAVE, VORTEX,
  RINGS, CONCENTRIC_RECTS, CROSS_STITCH,
  BLOOM, CHEVRONS, SPIRAL_ARMS, WINDMILL,
  ALL_PATTERNS,
  randomPattern, // generates a unique random pattern each call
} from 'clockboard'

| Pattern | Description | |---------|-------------| | NOON | All hands at 12 o'clock — clean reset | | RADIAL | Hands radiate outward from grid center — starburst | | CONVERGE | All hands point inward toward center — vortex sink | | CHECKERBOARD | Alternating vertical and horizontal bars | | WAVE | Sine wave undulating across columns | | VORTEX | Radial direction twisted by distance — galaxy spiral | | RINGS | Angle grows with distance from center — concentric rings | | CONCENTRIC_RECTS | Rectangular borders from edge to center | | CROSS_STITCH | Alternating X and + shapes — woven texture | | BLOOM | Hands splay wider with distance — flower opening | | CHEVRONS | Rows of V-shapes alternating direction | | SPIRAL_ARMS | Twisted V-shapes following a spiral curve | | WINDMILL | 2×2 repeating windmill — each cell rotated 90° from neighbors |

Using the pattern Prop

import { useState } from 'react'
import { ClockBoard, RADIAL, VORTEX, NOON } from 'clockboard'

function App() {
  const [pattern, setPattern] = useState(RADIAL)

  return (
    <>
      <ClockBoard pattern={pattern} duration={3000} />
      <button onClick={() => setPattern(RADIAL)}>Radial</button>
      <button onClick={() => setPattern(VORTEX)}>Vortex</button>
      <button onClick={() => setPattern(NOON)}>Reset</button>
    </>
  )
}

Time Composition

import { ClockBoard, composeTime } from 'clockboard'

// Static display of 14:30
<ClockBoard pattern={composeTime(14, 30)} />

composeTime(hours, minutes) returns a full GridPattern with the time rendered in the center 18×6 region.

Custom Patterns

import type { GridPattern, ClockState } from 'clockboard'

const myPattern: GridPattern = Array.from({ length: 8 }, (_, row) =>
  Array.from({ length: 20 }, (_, col): ClockState => {
    const angle = (row * 20 + col) * (360 / 160)
    return [angle, angle + 180]
  })
)

<ClockBoard pattern={myPattern} />

Grid Dimensions

The board is a fixed 20 columns × 8 rows (160 clocks).

Time layout within the grid:

  • The outermost ring of clocks (border) stays in idle position (both hands at 225°)
  • The inner 18×6 region displays content
  • Each digit occupies a 4×6 block
  • The colon between hours and minutes is a 2×4 block
  • Layout: [border] [digit] [digit] [colon] [digit] [digit] [border]

Multiple Instances

Each ClockBoard is fully independent:

<ClockBoard behavior={clock()} boardColor="#1a1a2e" handColor="#e94560" />
<ClockBoard behavior={random()} boardColor="#0f3460" handColor="#16213e" />
<ClockBoard pattern={RADIAL} boardColor="#222" faceColor="#333" />

Types

import type {
  ClockState,      // [hand1: number, hand2: number]
  GridPattern,     // ClockState[][] (8 rows × 20 cols)
  ApplyPattern,    // (pattern: GridPattern, duration?: number) => void
  Behavior,        // (apply: ApplyPattern) => () => void
  ClockBoardProps,
} from 'clockboard'

Full-Screen Display

<!DOCTYPE html>
<html>
<head><style>body { margin: 0; background: #171717; }</style></head>
<body>
  <div id="root"></div>
  <script type="module">
    import { createRoot } from 'react-dom/client'
    import { ClockBoard } from 'clockboard'
    createRoot(document.getElementById('root')).render(
      <ClockBoard style={{ height: '100vh' }} />
    )
  </script>
</body>
</html>

Set the body background to match boardColor so the display is seamless edge to edge.

Changelog

0.1.0

  • Overhauled animation engine for reliable, uninterrupted transitions
  • Fixed animation timing when tab loses focus or window is backgrounded
  • Added gradient support for boardColor
  • Fixed resizing issues

0.0.1

  • Initial release

License

MIT