KQ-1305 · WEEK/PLANNER
Week Planner
A week drawn on its hours. Pressing empty time and dragging rubber-bands a block out of the pressed slot, its far edge 1:1 under the pointer with a snapped line and a live time label, ticking at every new slot; on release the edge springs onto the snapped time on snap with the pointer's velocity and the event thocks down with its title ready to type. Events drag across time and days and stretch from either edge the same way, landing on snap with the release velocity while a dashed placeholder shows the landing and overlapping events re-divide their columns on glide; a now line moves through today on drift. The grid is one tab stop with a slot cursor (arrows, Shift for a range, Enter to create), and each event moves with the arrows, stretches with Shift, renames with Enter and goes with Delete.
Thu 1 Oct28 Sep – 4 Oct 2026
Up and Down move the event by 15 minutes, Shift with Up or Down changes its end, Left and Right move it a day, Enter renames, Delete removes, Escape returns to the grid.
Arrow keys move the slot cursor, Shift with Up or Down selects a range, Enter creates an event there, t jumps to now.
13:30 · 22 events this week · drag across empty time to add
Install
One command — the source lands in your repo. Or copy it from the Code tab.
$ pnpm dlx shadcn@latest add @kinetiq/week-planner
Props
- snap
- number
- default 15
- Minutes every drag, resize and cursor step snaps to; also the gutter's tick marks.
- density
- "compact" | "cozy" | "roomy"
- default "cozy"
- Height of an hour (40, 52 or 68px), with type and padding to match. A change glides every event to its new place.
- now
- Date | number
- default defaultWeekPlannerNow
- The current moment, which draws the now line and shades time already gone: a Date or epoch ms, or a number from 0 to 24 read as that hour of today (13.5 is 13:30).
- events / defaultEvents
- WeekEvent[]
- default defaultWeekEvents
- Events with epoch-ms start and end, a title, an optional calendar id and location. Controlled or uncontrolled.
- onEventsChange
- (events: WeekEvent[]) => void
- default —
- Fires from the drag, key or field that changed the events, with all of them.
- calendars
- WeekCalendar[]
- default defaultWeekCalendars
- The calendars events belong to, each with a label and a tint (a CSS colour token).
- day / defaultDay
- Date | number
- default today
- The focused day: its week is shown, and on a phone the day itself. Controlled or uncontrolled.
- onDayChange
- (day: number) => void
- default —
- Fires with the new day's midnight (epoch ms) from the arrows, the day strip, the headers or the keys.
- selected / defaultSelected
- string | null
- default null
- The selected event, shown with a stronger tint and, from 68rem, in the details panel.
- onSelectedChange
- (id: string | null) => void
- default —
- Fires when an event is picked, created or let go.
- today
- Date | number
- default the date of now
- The date marked as today; an hour-form now falls on it.
- hours
- [number, number]
- default [7, 21]
- The hours the grid draws. Events outside are clipped to them.
- workHours
- [number, number]
- default [9, 18]
- Working hours, drawn on the lighter ground; the keyboard cursor starts at their first hour when now is elsewhere.
- weekStartsOn
- 0 | 1
- default 1
- 1 starts weeks on Monday, 0 on Sunday.
- zoneOffset
- number
- default 0
- Minutes east of UTC that times are drawn in, never the machine's own zone, so the server and the browser draw the same week.
- defaultDuration
- number
- default 30
- Minutes an event made with Enter or New lasts when no range is selected.
- newTitle
- string
- default "New event"
- A new event's title, selected in its field so typing replaces it.
- defaultCalendar
- string
- default —
- The calendar new events join. Defaults to the first calendar.
- onCreate
- (event: WeekEvent) => void
- default —
- An event was drawn, made with Enter, or added with New.
- onUpdate
- (event: WeekEvent, previous: WeekEvent) => void
- default —
- An event moved, was resized, renamed or changed calendar.
- onDelete
- (event: WeekEvent) => void
- default —
- An event was removed with Delete, its close button or the details panel.
- label
- string
- default "Week"
- The planner's accessible name.
- status
- "ready" | "loading" | "error"
- default "ready"
- Loading covers the hours with a note; error offers Retry.
- onRetry
- () => void
- default —
- Retry was pressed on the error state.
- sound
- boolean
- default false
- Play a tick at every new slot a drag or key reaches and a thock when an event lands. Off unless asked for.
- disabled
- boolean
- default false
- Makes the whole planner inert.
- className
- string
- default —
- Classes for the root; it is 560px tall by default and lays out by its own width.
| Prop | Type | Default | Description |
|---|---|---|---|
| snap | number | 15 | Minutes every drag, resize and cursor step snaps to; also the gutter's tick marks. |
| density | "compact" | "cozy" | "roomy" | "cozy" | Height of an hour (40, 52 or 68px), with type and padding to match. A change glides every event to its new place. |
| now | Date | number | defaultWeekPlannerNow | The current moment, which draws the now line and shades time already gone: a Date or epoch ms, or a number from 0 to 24 read as that hour of today (13.5 is 13:30). |
| events / defaultEvents | WeekEvent[] | defaultWeekEvents | Events with epoch-ms start and end, a title, an optional calendar id and location. Controlled or uncontrolled. |
| onEventsChange | (events: WeekEvent[]) => void | — | Fires from the drag, key or field that changed the events, with all of them. |
| calendars | WeekCalendar[] | defaultWeekCalendars | The calendars events belong to, each with a label and a tint (a CSS colour token). |
| day / defaultDay | Date | number | today | The focused day: its week is shown, and on a phone the day itself. Controlled or uncontrolled. |
| onDayChange | (day: number) => void | — | Fires with the new day's midnight (epoch ms) from the arrows, the day strip, the headers or the keys. |
| selected / defaultSelected | string | null | null | The selected event, shown with a stronger tint and, from 68rem, in the details panel. |
| onSelectedChange | (id: string | null) => void | — | Fires when an event is picked, created or let go. |
| today | Date | number | the date of now | The date marked as today; an hour-form now falls on it. |
| hours | [number, number] | [7, 21] | The hours the grid draws. Events outside are clipped to them. |
| workHours | [number, number] | [9, 18] | Working hours, drawn on the lighter ground; the keyboard cursor starts at their first hour when now is elsewhere. |
| weekStartsOn | 0 | 1 | 1 | 1 starts weeks on Monday, 0 on Sunday. |
| zoneOffset | number | 0 | Minutes east of UTC that times are drawn in, never the machine's own zone, so the server and the browser draw the same week. |
| defaultDuration | number | 30 | Minutes an event made with Enter or New lasts when no range is selected. |
| newTitle | string | "New event" | A new event's title, selected in its field so typing replaces it. |
| defaultCalendar | string | — | The calendar new events join. Defaults to the first calendar. |
| onCreate | (event: WeekEvent) => void | — | An event was drawn, made with Enter, or added with New. |
| onUpdate | (event: WeekEvent, previous: WeekEvent) => void | — | An event moved, was resized, renamed or changed calendar. |
| onDelete | (event: WeekEvent) => void | — | An event was removed with Delete, its close button or the details panel. |
| label | string | "Week" | The planner's accessible name. |
| status | "ready" | "loading" | "error" | "ready" | Loading covers the hours with a note; error offers Retry. |
| onRetry | () => void | — | Retry was pressed on the error state. |
| sound | boolean | false | Play a tick at every new slot a drag or key reaches and a thock when an event lands. Off unless asked for. |
| disabled | boolean | false | Makes the whole planner inert. |
| className | string | — | Classes for the root; it is 560px tall by default and lays out by its own width. |