Components
Headless, composable numeric input field with drag-to-scrub, keyboard stepping, format/parse controls, and committed-value events for building any unit field.

"use client";
import * as React from "react";
import { UnitField } from "@/components/ui/unit-field";
const INPUT =
"w-full bg-transparent pr-2.5 text-right font-mono text-sm text-gray-700 tabular-nums outline-none placeholder:text-gray-300";
const DRAG_AREA =
"flex aspect-square w-9 shrink-0 cursor-ew-resize items-center justify-center text-[12px] font-medium text-gray-400 select-none";
const ROOT =
"relative flex h-9 w-full items-stretch border border-gray-200 transition-colors data-[focused=true]:border-gray-400";
function pctFormat(v: number) {
return `${v}%`;
}
function pctParse(v: string) {
const n = parseInt(v.replace("%", ""), 10);
return isNaN(n) ? null : n;
}
export default function UnitFieldCommittedDemo() {
const [value, setValue] = React.useState(50);
const [committed, setCommitted] = React.useState(50);
const handleCommitted = React.useCallback((v: number) => {
setCommitted(v);
}, []);
return (
<div className="mx-auto w-full max-w-sm bg-white p-6 text-gray-700">
<header className="mb-3">
<div className="flex items-center gap-2">
<span className="font-mono text-[11px] text-gray-400">03</span>
<h3 className="text-[14px] font-medium text-black">
Committed value
</h3>
</div>
<p className="mt-1 text-[13px] text-gray-500">
Live value updates on drag, committed value fires on release.
</p>
</header>
<UnitField.Root
className={ROOT}
format={pctFormat}
parse={pctParse}
value={value}
onValueChange={setValue}
onValueCommitted={handleCommitted}
min={0}
max={100}
>
<UnitField.DragArea className={DRAG_AREA}>%</UnitField.DragArea>
<UnitField.Input className={INPUT} />
</UnitField.Root>
<div className="mt-3 space-y-1.5">
<div className="flex items-center justify-between border border-gray-200 px-3 py-2">
<span className="text-[12px] text-gray-400">Live</span>
<span className="font-mono text-[13px] text-gray-600 tabular-nums">
{value}%
</span>
</div>
<div className="flex items-center justify-between border border-gray-200 px-3 py-2">
<span className="text-[12px] text-gray-400">Committed</span>
<span className="font-mono text-[13px] text-gray-600 tabular-nums">
{committed}%
</span>
</div>
</div>
<div className="mt-3 border border-gray-200">
<div
className="h-1.5 bg-gray-800 transition-all duration-75"
style={{ width: `${value}%` }}
/>
</div>
</div>
);
}