Duration Picker
A spring-animated picker for entering a duration in hours and minutes.
Loading component source...
Installation
pnpm dlx shadcn@latest add mox/mox-ui/duration-pickerRun this command from your project directory to add the component and its dependencies.
- 1
Install the packages
pnpm add motion figma-squircle flubber react-use-measure @radix-ui/react-slot clsx tailwind-merge - 2
Add the utility file
lib/utils.tsimport { clsx, type ClassValue } from "clsx";import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs));} - 3
Copy and paste the code into your project
Loading source⦠- 4
Update the import paths to match your project
Point
@/lib/utilsto your utility file if your path aliases differ.
Usage
"use client"
import DurationPicker, { type DurationValue } from "@/components/ui/duration-picker"import { useState } from "react"
export function Demo() { const [duration, setDuration] = useState<DurationValue>({ hours: 1, minutes: 30 })
return ( <DurationPicker value={duration} onChange={setDuration} // fires while typing onConfirm={(d) => console.log("saved", d)} // fires when the tick is clicked /> )}
// Zero-config: it also works fully uncontrolled// <DurationPicker onConfirm={(d) => console.log("saved", d)} />Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | { hours: number; minutes: number } | Default: - | Controlled value. Pair with onChange and the picker will mirror whatever you pass in. Leave it out to let the component manage its own state. |
defaultValue | { hours: number; minutes: number } | Default: { hours: 0, minutes: 0 } | Starting value for uncontrolled usage. Ignored when value is provided. |
onChange | (value: DurationValue) => void | Default: - | Fires on every keystroke with the current clamped value. Listen here if you want to react while the user types. |
onConfirm | (value: DurationValue) => void | Default: - | Fires once with the final value when the tick is clicked. This is usually the one you want for saving. |
onEditingChange | (editing: boolean) => void | Default: - | Notifies you when the picker enters or leaves edit mode. Handy for blocking navigation or dimming surrounding UI while open. |
defaultEditing | boolean | Default: false | Render the picker already open in edit mode. |
maxHours | number | Default: 24 | Ceiling for the hours field. Typing past it clamps to this value and shakes the input. |
maxMinutes | number | Default: 60 | Ceiling for the minutes field. Same clamp-and-shake behavior as maxHours. |
hoursLabel | string | Default: "Hr." | Text rendered after the hours field. Swap it for a translation or a terser 'h'. |
minutesLabel | string | Default: "Min." | Text rendered after the minutes field. |
disabled | boolean | Default: false | Dims the control and blocks entering edit mode. Standard form-field behavior. |
className | string | Default: - | Extra classes merged onto the root. Every inner part also carries a data-slot attribute (duration-picker, -segment, -input, -toggle) plus data-editing / data-disabled states, so you can restyle from CSS alone. |
Click the pen to spring the segments apart and start editing; the hours field is focused for you. Type your values; anything past the ceiling (24 hr / 60 min by default) clamps to the max and shakes so you know it was corrected. Click the tick to confirm and watch the pill merge back into one piece.
Credits and license
Most components here are recreations of great work from around the web. I don't claim to be the original creator - this is my attempt to reverse-engineer, replicate, and often add a few extra features. I've tried to credit everyone; if I missed someone, let me know.
- Free to use and modify in both personal and commercial projects.
- Attribution to Mox UI is appreciated when using a component.
- Please don't resell the components as your own kit.