{
- if (e.key !== "Tab" && e.key !== "Escape") onInteract();
+ ref={scroller}
+ role="listbox"
+ aria-label={label}
+ aria-disabled={!active}
+ aria-activedescendant={`${id}-${centerIndex}`}
+ tabIndex={active ? 0 : -1}
+ className="relative h-[200px] snap-y snap-mandatory overflow-y-auto overscroll-contain py-[80px] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden focus-visible:outline-2 focus-visible:outline-primary"
+ onPointerDown={beginScroll}
+ onTouchStart={beginScroll}
+ onWheel={beginScroll}
+ onScroll={event => {
+ setCenterIndex(Math.max(0, Math.min(rows.length - 1, Math.round(event.currentTarget.scrollTop / ROW_HEIGHT))));
+ if (active && userScroll.current) onScrollingChange(label, true);
+ }}
+ onScrollEnd={event => {
+ if (!active || !userScroll.current) return;
+ if (Math.abs(event.currentTarget.scrollTop - gestureStartTop.current) < ROW_HEIGHT / 2) {
+ userScroll.current = false;
+ onScrollingChange(label, false);
+ return;
+ }
+ const index = Math.max(0, Math.min(rows.length - 1, Math.round(event.currentTarget.scrollTop / ROW_HEIGHT)));
+ select(index);
+ }}
+ onKeyDown={event => {
+ const offset = event.key === "ArrowDown" ? 1 : event.key === "ArrowUp" ? -1 : 0;
+ if (!active || (!offset && event.key !== "Enter" && event.key !== " ")) return;
+ event.preventDefault();
+ select(Math.max(0, Math.min(rows.length - 1, centerIndex + offset)));
}}
>
-
-
-
- onChange(joinHm(next, hour, minute))}
- />
-
-
- onChange(joinHm(period, next, minute))}
- />
-
-
- onChange(joinHm(period, hour, next))}
- />
-
-
+ {rows.map((option, index) => (
+
+ ))}
+
+ );
+}
+
+/** Preview alignment is visual only. Each missing column requires a user selection. */
+export function TimeWheelPicker({ value, onChange, active, onSettlingChange }: Readonly<{
+ value: TimeParts;
+ onChange: (parts: TimeParts) => void;
+ active: boolean;
+ onSettlingChange: (settling: boolean) => void;
+}>) {
+ const scrollingColumns = useRef(new Set