scrubtime 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -41,8 +41,13 @@ function DraggableValue({
41
41
  }) {
42
42
  const [isEditing, setIsEditing] = (0, import_react.useState)(false);
43
43
  const [editValue, setEditValue] = (0, import_react.useState)("");
44
+ const [showMagnifier, setShowMagnifier] = (0, import_react.useState)(false);
45
+ const enteredViaKeyboard = (0, import_react.useRef)(false);
44
46
  const isDragging = (0, import_react.useRef)(false);
45
47
  const hasDragged = (0, import_react.useRef)(false);
48
+ const isLongPress = (0, import_react.useRef)(false);
49
+ const needsBuffer = (0, import_react.useRef)(false);
50
+ const longPressTimer = (0, import_react.useRef)(null);
46
51
  const startX = (0, import_react.useRef)(0);
47
52
  const startY = (0, import_react.useRef)(0);
48
53
  const lastX = (0, import_react.useRef)(0);
@@ -94,34 +99,76 @@ function DraggableValue({
94
99
  const touch = e.touches[0];
95
100
  isDragging.current = true;
96
101
  hasDragged.current = false;
102
+ isLongPress.current = false;
97
103
  startX.current = touch.clientX;
98
104
  startY.current = touch.clientY;
99
105
  lastX.current = touch.clientX;
100
106
  lastY.current = touch.clientY;
101
107
  accumulatedDelta.current = 0;
108
+ longPressTimer.current = setTimeout(() => {
109
+ isLongPress.current = true;
110
+ setShowMagnifier(true);
111
+ }, 300);
102
112
  const handleTouchMove = (e2) => {
103
113
  if (!isDragging.current) return;
104
114
  const touch2 = e2.touches[0];
105
- const totalDeltaX = Math.abs(touch2.clientX - startX.current);
106
- const totalDeltaY = Math.abs(touch2.clientY - startY.current);
107
- if (totalDeltaX > DRAG_THRESHOLD || totalDeltaY > DRAG_THRESHOLD) {
108
- hasDragged.current = true;
109
- e2.preventDefault();
115
+ if (!hasDragged.current) {
116
+ const totalDeltaX = touch2.clientX - startX.current;
117
+ const totalDeltaY = touch2.clientY - startY.current;
118
+ if (isLongPress.current) {
119
+ const deltaX2 = touch2.clientX - lastX.current;
120
+ const deltaY2 = touch2.clientY - lastY.current;
121
+ if (Math.abs(deltaX2) > 0 || Math.abs(deltaY2) > 0) {
122
+ hasDragged.current = true;
123
+ e2.preventDefault();
124
+ const direction = deltaX2 + deltaY2 > 0 ? 1 : -1;
125
+ onDeltaRef.current(direction);
126
+ lastX.current = touch2.clientX;
127
+ lastY.current = touch2.clientY;
128
+ accumulatedDelta.current = -direction * 0.8;
129
+ return;
130
+ }
131
+ }
132
+ if (Math.abs(totalDeltaX) > DRAG_THRESHOLD || Math.abs(totalDeltaY) > DRAG_THRESHOLD) {
133
+ if (longPressTimer.current) {
134
+ clearTimeout(longPressTimer.current);
135
+ longPressTimer.current = null;
136
+ }
137
+ hasDragged.current = true;
138
+ needsBuffer.current = true;
139
+ setShowMagnifier(true);
140
+ e2.preventDefault();
141
+ lastX.current = touch2.clientX;
142
+ lastY.current = touch2.clientY;
143
+ accumulatedDelta.current = 0;
144
+ }
145
+ lastX.current = touch2.clientX;
146
+ lastY.current = touch2.clientY;
147
+ return;
110
148
  }
149
+ e2.preventDefault();
111
150
  const deltaX = touch2.clientX - lastX.current;
112
151
  const deltaY = touch2.clientY - lastY.current;
113
- const deltaValue = (deltaX - deltaY) / sensitivity;
152
+ const deltaValue = (deltaX + deltaY) / sensitivity;
114
153
  accumulatedDelta.current += deltaValue;
115
- const wholeDelta = Math.trunc(accumulatedDelta.current);
116
- if (wholeDelta !== 0) {
154
+ const threshold = needsBuffer.current ? 2 : 1;
155
+ if (Math.abs(accumulatedDelta.current) >= threshold) {
156
+ const wholeDelta = Math.trunc(accumulatedDelta.current);
117
157
  accumulatedDelta.current -= wholeDelta;
118
158
  onDeltaRef.current(wholeDelta);
159
+ needsBuffer.current = false;
119
160
  }
120
161
  lastX.current = touch2.clientX;
121
162
  lastY.current = touch2.clientY;
122
163
  };
123
164
  const handleTouchEnd = () => {
124
165
  isDragging.current = false;
166
+ needsBuffer.current = false;
167
+ if (longPressTimer.current) {
168
+ clearTimeout(longPressTimer.current);
169
+ longPressTimer.current = null;
170
+ }
171
+ setShowMagnifier(false);
125
172
  document.removeEventListener("touchmove", handleTouchMove);
126
173
  document.removeEventListener("touchend", handleTouchEnd);
127
174
  };
@@ -133,6 +180,7 @@ function DraggableValue({
133
180
  const displayValue = formatValue ? formatValue(value) : String(value);
134
181
  const handleClick = (0, import_react.useCallback)(() => {
135
182
  if (disabled || hasDragged.current) return;
183
+ enteredViaKeyboard.current = false;
136
184
  setEditValue(displayValue);
137
185
  setIsEditing(true);
138
186
  }, [disabled, displayValue]);
@@ -141,6 +189,7 @@ function DraggableValue({
141
189
  if (disabled) return;
142
190
  if (/^[0-9]$/.test(e.key)) {
143
191
  e.preventDefault();
192
+ enteredViaKeyboard.current = true;
144
193
  setEditValue(e.key);
145
194
  setIsEditing(true);
146
195
  }
@@ -165,6 +214,11 @@ function DraggableValue({
165
214
  },
166
215
  [commitEdit]
167
216
  );
217
+ const handleInputFocus = (0, import_react.useCallback)((e) => {
218
+ if (!enteredViaKeyboard.current) {
219
+ e.target.select();
220
+ }
221
+ }, []);
168
222
  if (isEditing) {
169
223
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
170
224
  "input",
@@ -173,7 +227,7 @@ function DraggableValue({
173
227
  inputMode: "numeric",
174
228
  value: editValue,
175
229
  onChange: (e) => setEditValue(e.target.value.replace(/\D/g, "").slice(-2)),
176
- onFocus: (e) => e.target.select(),
230
+ onFocus: handleInputFocus,
177
231
  onBlur: commitEdit,
178
232
  onKeyDown: handleKeyDown,
179
233
  className: `scrubtime-value scrubtime-value--editing ${className || ""}`,
@@ -181,21 +235,24 @@ function DraggableValue({
181
235
  }
182
236
  );
183
237
  }
184
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
185
- "div",
186
- {
187
- onMouseDown: handleMouseDown,
188
- onTouchStart: handleTouchStart,
189
- onClick: handleClick,
190
- onKeyDown: handleDivKeyDown,
191
- className: `scrubtime-value ${className || ""} ${disabled ? "scrubtime-value--disabled" : ""}`,
192
- role: "spinbutton",
193
- "aria-valuenow": value,
194
- "aria-disabled": disabled,
195
- tabIndex: disabled ? -1 : 0,
196
- children: displayValue
197
- }
198
- );
238
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "scrubtime-value-wrapper", children: [
239
+ showMagnifier && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "scrubtime-magnifier", "aria-hidden": "true", children: displayValue }),
240
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
241
+ "div",
242
+ {
243
+ onMouseDown: handleMouseDown,
244
+ onTouchStart: handleTouchStart,
245
+ onClick: handleClick,
246
+ onKeyDown: handleDivKeyDown,
247
+ className: `scrubtime-value ${className || ""} ${disabled ? "scrubtime-value--disabled" : ""}`,
248
+ role: "spinbutton",
249
+ "aria-valuenow": value,
250
+ "aria-disabled": disabled,
251
+ tabIndex: disabled ? -1 : 0,
252
+ children: displayValue
253
+ }
254
+ )
255
+ ] });
199
256
  }
200
257
  function parseTime(time) {
201
258
  const [h, m] = time.split(":").map(Number);
@@ -281,7 +338,7 @@ function TimePicker({
281
338
  onSet: handleMinutesSet,
282
339
  formatValue: (v) => String(v).padStart(2, "0"),
283
340
  disabled,
284
- sensitivity: dragSensitivity / 2,
341
+ sensitivity: dragSensitivity,
285
342
  min: 0,
286
343
  max: 59,
287
344
  className: "scrubtime-minutes"
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/TimePicker.tsx"],"sourcesContent":["export { TimePicker } from './TimePicker';\nexport type { TimePickerProps } from './TimePicker';\n","import { useRef, useCallback, useState } from 'react';\n\nexport interface TimePickerProps {\n /** Current time value in \"H:mm\" or \"HH:mm\" format */\n value: string;\n /** Callback fired when time changes */\n onChange: (value: string) => void;\n /** Optional label displayed above the picker */\n label?: string;\n /** Custom class name for the root element */\n className?: string;\n /** Disable the picker */\n disabled?: boolean;\n /** Slider step in minutes (default: 15) */\n sliderStep?: number;\n /** Drag sensitivity - pixels per unit change (default: 3) */\n dragSensitivity?: number;\n /** Number of equal parts to divide the 24h range into (default: 4 = labels at 0,6,12,18,24) */\n divisions?: number;\n}\n\ninterface DraggableValueProps {\n value: number;\n onDelta: (delta: number) => void;\n onSet: (value: number) => void;\n formatValue?: (v: number) => string;\n className?: string;\n disabled?: boolean;\n sensitivity: number;\n min: number;\n max: number;\n}\n\nconst DRAG_THRESHOLD = 3;\n\nfunction DraggableValue({\n value,\n onDelta,\n onSet,\n formatValue,\n className,\n disabled,\n sensitivity,\n min,\n max,\n}: DraggableValueProps) {\n const [isEditing, setIsEditing] = useState(false);\n const [editValue, setEditValue] = useState('');\n const isDragging = useRef(false);\n const hasDragged = useRef(false);\n const startX = useRef(0);\n const startY = useRef(0);\n const lastX = useRef(0);\n const lastY = useRef(0);\n const accumulatedDelta = useRef(0);\n const onDeltaRef = useRef(onDelta);\n onDeltaRef.current = onDelta;\n\n const handleMouseDown = useCallback(\n (e: React.MouseEvent) => {\n if (disabled || isEditing) return;\n\n isDragging.current = true;\n hasDragged.current = false;\n startX.current = e.clientX;\n lastX.current = e.clientX;\n accumulatedDelta.current = 0;\n document.body.style.cursor = 'ew-resize';\n document.body.style.userSelect = 'none';\n\n const handleMouseMove = (e: MouseEvent) => {\n if (!isDragging.current) return;\n\n const totalDeltaX = Math.abs(e.clientX - startX.current);\n if (totalDeltaX > DRAG_THRESHOLD) {\n hasDragged.current = true;\n }\n\n const deltaX = e.clientX - lastX.current;\n const deltaValue = deltaX / sensitivity;\n accumulatedDelta.current += deltaValue;\n\n const wholeDelta = Math.trunc(accumulatedDelta.current);\n if (wholeDelta !== 0) {\n accumulatedDelta.current -= wholeDelta;\n onDeltaRef.current(wholeDelta);\n }\n lastX.current = e.clientX;\n };\n\n const handleMouseUp = () => {\n isDragging.current = false;\n document.body.style.cursor = '';\n document.body.style.userSelect = '';\n document.removeEventListener('mousemove', handleMouseMove);\n document.removeEventListener('mouseup', handleMouseUp);\n };\n\n document.addEventListener('mousemove', handleMouseMove);\n document.addEventListener('mouseup', handleMouseUp);\n },\n [disabled, sensitivity, isEditing]\n );\n\n const handleTouchStart = useCallback(\n (e: React.TouchEvent) => {\n if (disabled || isEditing) return;\n\n const touch = e.touches[0];\n isDragging.current = true;\n hasDragged.current = false;\n startX.current = touch.clientX;\n startY.current = touch.clientY;\n lastX.current = touch.clientX;\n lastY.current = touch.clientY;\n accumulatedDelta.current = 0;\n\n const handleTouchMove = (e: TouchEvent) => {\n if (!isDragging.current) return;\n\n const touch = e.touches[0];\n const totalDeltaX = Math.abs(touch.clientX - startX.current);\n const totalDeltaY = Math.abs(touch.clientY - startY.current);\n if (totalDeltaX > DRAG_THRESHOLD || totalDeltaY > DRAG_THRESHOLD) {\n hasDragged.current = true;\n e.preventDefault(); // Prevent scrolling once dragging starts\n }\n\n const deltaX = touch.clientX - lastX.current;\n const deltaY = touch.clientY - lastY.current;\n // Horizontal: right = increase, left = decrease\n // Vertical: up = increase, down = decrease\n const deltaValue = (deltaX - deltaY) / sensitivity;\n accumulatedDelta.current += deltaValue;\n\n const wholeDelta = Math.trunc(accumulatedDelta.current);\n if (wholeDelta !== 0) {\n accumulatedDelta.current -= wholeDelta;\n onDeltaRef.current(wholeDelta);\n }\n lastX.current = touch.clientX;\n lastY.current = touch.clientY;\n };\n\n const handleTouchEnd = () => {\n isDragging.current = false;\n document.removeEventListener('touchmove', handleTouchMove);\n document.removeEventListener('touchend', handleTouchEnd);\n };\n\n document.addEventListener('touchmove', handleTouchMove, { passive: false });\n document.addEventListener('touchend', handleTouchEnd);\n },\n [disabled, sensitivity, isEditing]\n );\n\n const displayValue = formatValue ? formatValue(value) : String(value);\n\n const handleClick = useCallback(() => {\n if (disabled || hasDragged.current) return;\n setEditValue(displayValue);\n setIsEditing(true);\n }, [disabled, displayValue]);\n\n const handleDivKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (disabled) return;\n if (/^[0-9]$/.test(e.key)) {\n e.preventDefault();\n setEditValue(e.key);\n setIsEditing(true);\n }\n },\n [disabled]\n );\n\n const commitEdit = useCallback(() => {\n const parsed = parseInt(editValue, 10);\n if (!isNaN(parsed)) {\n const clamped = Math.max(min, Math.min(max, parsed));\n onSet(clamped);\n }\n setIsEditing(false);\n }, [editValue, min, max, onSet]);\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (e.key === 'Enter') {\n commitEdit();\n } else if (e.key === 'Escape') {\n setIsEditing(false);\n }\n },\n [commitEdit]\n );\n\n if (isEditing) {\n return (\n <input\n type=\"text\"\n inputMode=\"numeric\"\n value={editValue}\n onChange={(e) => setEditValue(e.target.value.replace(/\\D/g, '').slice(-2))}\n onFocus={(e) => e.target.select()}\n onBlur={commitEdit}\n onKeyDown={handleKeyDown}\n className={`scrubtime-value scrubtime-value--editing ${className || ''}`}\n autoFocus\n />\n );\n }\n\n return (\n <div\n onMouseDown={handleMouseDown}\n onTouchStart={handleTouchStart}\n onClick={handleClick}\n onKeyDown={handleDivKeyDown}\n className={`scrubtime-value ${className || ''} ${disabled ? 'scrubtime-value--disabled' : ''}`}\n role=\"spinbutton\"\n aria-valuenow={value}\n aria-disabled={disabled}\n tabIndex={disabled ? -1 : 0}\n >\n {displayValue}\n </div>\n );\n}\n\nfunction parseTime(time: string): { hours: number; minutes: number } {\n const [h, m] = time.split(':').map(Number);\n return { hours: h || 0, minutes: m || 0 };\n}\n\nfunction formatTime(hours: number, minutes: number): string {\n return `${hours}:${String(minutes).padStart(2, '0')}`;\n}\n\nfunction clampTotalMinutes(totalMins: number): number {\n return Math.max(0, Math.min(23 * 60 + 59, totalMins));\n}\n\nexport function TimePicker({\n value,\n onChange,\n label,\n className,\n disabled = false,\n sliderStep = 15,\n dragSensitivity = 3,\n divisions = 4,\n}: TimePickerProps) {\n const labelCount = divisions + 1;\n const { hours, minutes } = parseTime(value);\n const totalMinutes = hours * 60 + minutes;\n\n const handleSliderChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n if (disabled) return;\n const mins = clampTotalMinutes(parseInt(e.target.value));\n const h = Math.floor(mins / 60);\n const m = mins % 60;\n onChange(formatTime(h, m));\n };\n\n const handleHoursDelta = useCallback(\n (delta: number) => {\n const newHours = Math.max(0, Math.min(23, hours + delta));\n onChange(formatTime(newHours, minutes));\n },\n [hours, minutes, onChange]\n );\n\n const handleHoursSet = useCallback(\n (newHours: number) => {\n onChange(formatTime(newHours, minutes));\n },\n [minutes, onChange]\n );\n\n const handleMinutesDelta = useCallback(\n (delta: number) => {\n const newTotalMinutes = clampTotalMinutes(totalMinutes + delta);\n const h = Math.floor(newTotalMinutes / 60);\n const m = newTotalMinutes % 60;\n onChange(formatTime(h, m));\n },\n [totalMinutes, onChange]\n );\n\n const handleMinutesSet = useCallback(\n (newMinutes: number) => {\n onChange(formatTime(hours, newMinutes));\n },\n [hours, onChange]\n );\n\n return (\n <div className={`scrubtime ${className || ''} ${disabled ? 'scrubtime--disabled' : ''}`}>\n {label && <label className=\"scrubtime-label\">{label}</label>}\n\n <div className=\"scrubtime-container\">\n <div className=\"scrubtime-display\">\n <DraggableValue\n value={hours}\n onDelta={handleHoursDelta}\n onSet={handleHoursSet}\n disabled={disabled}\n sensitivity={dragSensitivity * 2}\n min={0}\n max={23}\n className=\"scrubtime-hours\"\n />\n <span className=\"scrubtime-separator\">:</span>\n <DraggableValue\n value={minutes}\n onDelta={handleMinutesDelta}\n onSet={handleMinutesSet}\n formatValue={(v) => String(v).padStart(2, '0')}\n disabled={disabled}\n sensitivity={dragSensitivity / 2}\n min={0}\n max={59}\n className=\"scrubtime-minutes\"\n />\n </div>\n\n <div className=\"scrubtime-slider-container\">\n <input\n type=\"range\"\n min={0}\n max={23 * 60 + 59}\n step={sliderStep}\n value={totalMinutes}\n onChange={handleSliderChange}\n disabled={disabled}\n tabIndex={-1}\n className=\"scrubtime-slider\"\n aria-label=\"Time slider\"\n />\n <div className=\"scrubtime-slider-labels\">\n {Array.from({ length: labelCount }, (_, i) => {\n const hour = Math.round((24 / divisions) * i);\n const percent = (i / divisions) * 100;\n return (\n <span key={i} style={{ left: `${percent}%` }}>\n {hour}\n </span>\n );\n })}\n </div>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":["e","touch"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/TimePicker.tsx"],"sourcesContent":["export { TimePicker } from './TimePicker';\nexport type { TimePickerProps } from './TimePicker';\n","import { useRef, useCallback, useState } from 'react';\n\nexport interface TimePickerProps {\n /** Current time value in \"H:mm\" or \"HH:mm\" format */\n value: string;\n /** Callback fired when time changes */\n onChange: (value: string) => void;\n /** Optional label displayed above the picker */\n label?: string;\n /** Custom class name for the root element */\n className?: string;\n /** Disable the picker */\n disabled?: boolean;\n /** Slider step in minutes (default: 15) */\n sliderStep?: number;\n /** Drag sensitivity - pixels per unit change (default: 3) */\n dragSensitivity?: number;\n /** Number of equal parts to divide the 24h range into (default: 4 = labels at 0,6,12,18,24) */\n divisions?: number;\n}\n\ninterface DraggableValueProps {\n value: number;\n onDelta: (delta: number) => void;\n onSet: (value: number) => void;\n formatValue?: (v: number) => string;\n className?: string;\n disabled?: boolean;\n sensitivity: number;\n min: number;\n max: number;\n}\n\nconst DRAG_THRESHOLD = 3;\n\nfunction DraggableValue({\n value,\n onDelta,\n onSet,\n formatValue,\n className,\n disabled,\n sensitivity,\n min,\n max,\n}: DraggableValueProps) {\n const [isEditing, setIsEditing] = useState(false);\n const [editValue, setEditValue] = useState('');\n const [showMagnifier, setShowMagnifier] = useState(false);\n const enteredViaKeyboard = useRef(false);\n const isDragging = useRef(false);\n const hasDragged = useRef(false);\n const isLongPress = useRef(false);\n const needsBuffer = useRef(false);\n const longPressTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n const startX = useRef(0);\n const startY = useRef(0);\n const lastX = useRef(0);\n const lastY = useRef(0);\n const accumulatedDelta = useRef(0);\n const onDeltaRef = useRef(onDelta);\n onDeltaRef.current = onDelta;\n\n const handleMouseDown = useCallback(\n (e: React.MouseEvent) => {\n if (disabled || isEditing) return;\n\n isDragging.current = true;\n hasDragged.current = false;\n startX.current = e.clientX;\n lastX.current = e.clientX;\n accumulatedDelta.current = 0;\n document.body.style.cursor = 'ew-resize';\n document.body.style.userSelect = 'none';\n\n const handleMouseMove = (e: MouseEvent) => {\n if (!isDragging.current) return;\n\n const totalDeltaX = Math.abs(e.clientX - startX.current);\n if (totalDeltaX > DRAG_THRESHOLD) {\n hasDragged.current = true;\n }\n\n const deltaX = e.clientX - lastX.current;\n const deltaValue = deltaX / sensitivity;\n accumulatedDelta.current += deltaValue;\n\n const wholeDelta = Math.trunc(accumulatedDelta.current);\n if (wholeDelta !== 0) {\n accumulatedDelta.current -= wholeDelta;\n onDeltaRef.current(wholeDelta);\n }\n lastX.current = e.clientX;\n };\n\n const handleMouseUp = () => {\n isDragging.current = false;\n document.body.style.cursor = '';\n document.body.style.userSelect = '';\n document.removeEventListener('mousemove', handleMouseMove);\n document.removeEventListener('mouseup', handleMouseUp);\n };\n\n document.addEventListener('mousemove', handleMouseMove);\n document.addEventListener('mouseup', handleMouseUp);\n },\n [disabled, sensitivity, isEditing]\n );\n\n const handleTouchStart = useCallback(\n (e: React.TouchEvent) => {\n if (disabled || isEditing) return;\n\n const touch = e.touches[0];\n isDragging.current = true;\n hasDragged.current = false;\n isLongPress.current = false;\n startX.current = touch.clientX;\n startY.current = touch.clientY;\n lastX.current = touch.clientX;\n lastY.current = touch.clientY;\n accumulatedDelta.current = 0;\n\n // Long press timer - activates after 300ms, no threshold needed after\n longPressTimer.current = setTimeout(() => {\n isLongPress.current = true;\n setShowMagnifier(true);\n }, 300);\n\n const handleTouchMove = (e: TouchEvent) => {\n if (!isDragging.current) return;\n\n const touch = e.touches[0];\n\n // Before drag is activated, check threshold or long press\n if (!hasDragged.current) {\n const totalDeltaX = touch.clientX - startX.current;\n const totalDeltaY = touch.clientY - startY.current;\n\n // Long press activated - start immediately with +1/-1 on any movement\n if (isLongPress.current) {\n const deltaX = touch.clientX - lastX.current;\n const deltaY = touch.clientY - lastY.current;\n if (Math.abs(deltaX) > 0 || Math.abs(deltaY) > 0) {\n hasDragged.current = true;\n e.preventDefault();\n const direction = (deltaX + deltaY) > 0 ? 1 : -1;\n onDeltaRef.current(direction);\n lastX.current = touch.clientX;\n lastY.current = touch.clientY;\n // Add buffer to prevent rapid-fire changes after activation\n accumulatedDelta.current = -direction * 0.8;\n return;\n }\n }\n\n if (Math.abs(totalDeltaX) > DRAG_THRESHOLD || Math.abs(totalDeltaY) > DRAG_THRESHOLD) {\n // Cancel long press timer since drag started\n if (longPressTimer.current) {\n clearTimeout(longPressTimer.current);\n longPressTimer.current = null;\n }\n hasDragged.current = true;\n needsBuffer.current = true;\n setShowMagnifier(true);\n e.preventDefault();\n // Show magnifier first, don't change value yet - further dragging will mutate\n lastX.current = touch.clientX;\n lastY.current = touch.clientY;\n accumulatedDelta.current = 0;\n }\n // Always update lastX/lastY so long press activation has fresh position\n lastX.current = touch.clientX;\n lastY.current = touch.clientY;\n return;\n }\n\n e.preventDefault();\n const deltaX = touch.clientX - lastX.current;\n const deltaY = touch.clientY - lastY.current;\n // Horizontal: right = increase, left = decrease\n // Vertical: up = increase, down = decrease\n const deltaValue = (deltaX + deltaY) / sensitivity;\n accumulatedDelta.current += deltaValue;\n\n // Require more movement for first change after distance activation\n const threshold = needsBuffer.current ? 2 : 1;\n if (Math.abs(accumulatedDelta.current) >= threshold) {\n const wholeDelta = Math.trunc(accumulatedDelta.current);\n accumulatedDelta.current -= wholeDelta;\n onDeltaRef.current(wholeDelta);\n needsBuffer.current = false;\n }\n lastX.current = touch.clientX;\n lastY.current = touch.clientY;\n };\n\n const handleTouchEnd = () => {\n isDragging.current = false;\n needsBuffer.current = false;\n if (longPressTimer.current) {\n clearTimeout(longPressTimer.current);\n longPressTimer.current = null;\n }\n setShowMagnifier(false);\n document.removeEventListener('touchmove', handleTouchMove);\n document.removeEventListener('touchend', handleTouchEnd);\n };\n\n document.addEventListener('touchmove', handleTouchMove, { passive: false });\n document.addEventListener('touchend', handleTouchEnd);\n },\n [disabled, sensitivity, isEditing]\n );\n\n const displayValue = formatValue ? formatValue(value) : String(value);\n\n const handleClick = useCallback(() => {\n if (disabled || hasDragged.current) return;\n enteredViaKeyboard.current = false;\n setEditValue(displayValue);\n setIsEditing(true);\n }, [disabled, displayValue]);\n\n const handleDivKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (disabled) return;\n if (/^[0-9]$/.test(e.key)) {\n e.preventDefault();\n enteredViaKeyboard.current = true;\n setEditValue(e.key);\n setIsEditing(true);\n }\n },\n [disabled]\n );\n\n const commitEdit = useCallback(() => {\n const parsed = parseInt(editValue, 10);\n if (!isNaN(parsed)) {\n const clamped = Math.max(min, Math.min(max, parsed));\n onSet(clamped);\n }\n setIsEditing(false);\n }, [editValue, min, max, onSet]);\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (e.key === 'Enter') {\n commitEdit();\n } else if (e.key === 'Escape') {\n setIsEditing(false);\n }\n },\n [commitEdit]\n );\n\n const handleInputFocus = useCallback((e: React.FocusEvent<HTMLInputElement>) => {\n if (!enteredViaKeyboard.current) {\n e.target.select();\n }\n }, []);\n\n if (isEditing) {\n return (\n <input\n type=\"text\"\n inputMode=\"numeric\"\n value={editValue}\n onChange={(e) => setEditValue(e.target.value.replace(/\\D/g, '').slice(-2))}\n onFocus={handleInputFocus}\n onBlur={commitEdit}\n onKeyDown={handleKeyDown}\n className={`scrubtime-value scrubtime-value--editing ${className || ''}`}\n autoFocus\n />\n );\n }\n\n return (\n <div className=\"scrubtime-value-wrapper\">\n {showMagnifier && (\n <div className=\"scrubtime-magnifier\" aria-hidden=\"true\">\n {displayValue}\n </div>\n )}\n <div\n onMouseDown={handleMouseDown}\n onTouchStart={handleTouchStart}\n onClick={handleClick}\n onKeyDown={handleDivKeyDown}\n className={`scrubtime-value ${className || ''} ${disabled ? 'scrubtime-value--disabled' : ''}`}\n role=\"spinbutton\"\n aria-valuenow={value}\n aria-disabled={disabled}\n tabIndex={disabled ? -1 : 0}\n >\n {displayValue}\n </div>\n </div>\n );\n}\n\nfunction parseTime(time: string): { hours: number; minutes: number } {\n const [h, m] = time.split(':').map(Number);\n return { hours: h || 0, minutes: m || 0 };\n}\n\nfunction formatTime(hours: number, minutes: number): string {\n return `${hours}:${String(minutes).padStart(2, '0')}`;\n}\n\nfunction clampTotalMinutes(totalMins: number): number {\n return Math.max(0, Math.min(23 * 60 + 59, totalMins));\n}\n\nexport function TimePicker({\n value,\n onChange,\n label,\n className,\n disabled = false,\n sliderStep = 15,\n dragSensitivity = 3,\n divisions = 4,\n}: TimePickerProps) {\n const labelCount = divisions + 1;\n const { hours, minutes } = parseTime(value);\n const totalMinutes = hours * 60 + minutes;\n\n const handleSliderChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n if (disabled) return;\n const mins = clampTotalMinutes(parseInt(e.target.value));\n const h = Math.floor(mins / 60);\n const m = mins % 60;\n onChange(formatTime(h, m));\n };\n\n const handleHoursDelta = useCallback(\n (delta: number) => {\n const newHours = Math.max(0, Math.min(23, hours + delta));\n onChange(formatTime(newHours, minutes));\n },\n [hours, minutes, onChange]\n );\n\n const handleHoursSet = useCallback(\n (newHours: number) => {\n onChange(formatTime(newHours, minutes));\n },\n [minutes, onChange]\n );\n\n const handleMinutesDelta = useCallback(\n (delta: number) => {\n const newTotalMinutes = clampTotalMinutes(totalMinutes + delta);\n const h = Math.floor(newTotalMinutes / 60);\n const m = newTotalMinutes % 60;\n onChange(formatTime(h, m));\n },\n [totalMinutes, onChange]\n );\n\n const handleMinutesSet = useCallback(\n (newMinutes: number) => {\n onChange(formatTime(hours, newMinutes));\n },\n [hours, onChange]\n );\n\n return (\n <div className={`scrubtime ${className || ''} ${disabled ? 'scrubtime--disabled' : ''}`}>\n {label && <label className=\"scrubtime-label\">{label}</label>}\n\n <div className=\"scrubtime-container\">\n <div className=\"scrubtime-display\">\n <DraggableValue\n value={hours}\n onDelta={handleHoursDelta}\n onSet={handleHoursSet}\n disabled={disabled}\n sensitivity={dragSensitivity * 2}\n min={0}\n max={23}\n className=\"scrubtime-hours\"\n />\n <span className=\"scrubtime-separator\">:</span>\n <DraggableValue\n value={minutes}\n onDelta={handleMinutesDelta}\n onSet={handleMinutesSet}\n formatValue={(v) => String(v).padStart(2, '0')}\n disabled={disabled}\n sensitivity={dragSensitivity}\n min={0}\n max={59}\n className=\"scrubtime-minutes\"\n />\n </div>\n\n <div className=\"scrubtime-slider-container\">\n <input\n type=\"range\"\n min={0}\n max={23 * 60 + 59}\n step={sliderStep}\n value={totalMinutes}\n onChange={handleSliderChange}\n disabled={disabled}\n tabIndex={-1}\n className=\"scrubtime-slider\"\n aria-label=\"Time slider\"\n />\n <div className=\"scrubtime-slider-labels\">\n {Array.from({ length: labelCount }, (_, i) => {\n const hour = Math.round((24 / divisions) * i);\n const percent = (i / divisions) * 100;\n return (\n <span key={i} style={{ left: `${percent}%` }}>\n {hour}\n </span>\n );\n })}\n </div>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,mBAA8C;AAyQxC;AAxON,IAAM,iBAAiB;AAEvB,SAAS,eAAe;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAwB;AACtB,QAAM,CAAC,WAAW,YAAY,QAAI,uBAAS,KAAK;AAChD,QAAM,CAAC,WAAW,YAAY,QAAI,uBAAS,EAAE;AAC7C,QAAM,CAAC,eAAe,gBAAgB,QAAI,uBAAS,KAAK;AACxD,QAAM,yBAAqB,qBAAO,KAAK;AACvC,QAAM,iBAAa,qBAAO,KAAK;AAC/B,QAAM,iBAAa,qBAAO,KAAK;AAC/B,QAAM,kBAAc,qBAAO,KAAK;AAChC,QAAM,kBAAc,qBAAO,KAAK;AAChC,QAAM,qBAAiB,qBAA6C,IAAI;AACxE,QAAM,aAAS,qBAAO,CAAC;AACvB,QAAM,aAAS,qBAAO,CAAC;AACvB,QAAM,YAAQ,qBAAO,CAAC;AACtB,QAAM,YAAQ,qBAAO,CAAC;AACtB,QAAM,uBAAmB,qBAAO,CAAC;AACjC,QAAM,iBAAa,qBAAO,OAAO;AACjC,aAAW,UAAU;AAErB,QAAM,sBAAkB;AAAA,IACtB,CAAC,MAAwB;AACvB,UAAI,YAAY,UAAW;AAE3B,iBAAW,UAAU;AACrB,iBAAW,UAAU;AACrB,aAAO,UAAU,EAAE;AACnB,YAAM,UAAU,EAAE;AAClB,uBAAiB,UAAU;AAC3B,eAAS,KAAK,MAAM,SAAS;AAC7B,eAAS,KAAK,MAAM,aAAa;AAEjC,YAAM,kBAAkB,CAACA,OAAkB;AACzC,YAAI,CAAC,WAAW,QAAS;AAEzB,cAAM,cAAc,KAAK,IAAIA,GAAE,UAAU,OAAO,OAAO;AACvD,YAAI,cAAc,gBAAgB;AAChC,qBAAW,UAAU;AAAA,QACvB;AAEA,cAAM,SAASA,GAAE,UAAU,MAAM;AACjC,cAAM,aAAa,SAAS;AAC5B,yBAAiB,WAAW;AAE5B,cAAM,aAAa,KAAK,MAAM,iBAAiB,OAAO;AACtD,YAAI,eAAe,GAAG;AACpB,2BAAiB,WAAW;AAC5B,qBAAW,QAAQ,UAAU;AAAA,QAC/B;AACA,cAAM,UAAUA,GAAE;AAAA,MACpB;AAEA,YAAM,gBAAgB,MAAM;AAC1B,mBAAW,UAAU;AACrB,iBAAS,KAAK,MAAM,SAAS;AAC7B,iBAAS,KAAK,MAAM,aAAa;AACjC,iBAAS,oBAAoB,aAAa,eAAe;AACzD,iBAAS,oBAAoB,WAAW,aAAa;AAAA,MACvD;AAEA,eAAS,iBAAiB,aAAa,eAAe;AACtD,eAAS,iBAAiB,WAAW,aAAa;AAAA,IACpD;AAAA,IACA,CAAC,UAAU,aAAa,SAAS;AAAA,EACnC;AAEA,QAAM,uBAAmB;AAAA,IACvB,CAAC,MAAwB;AACvB,UAAI,YAAY,UAAW;AAE3B,YAAM,QAAQ,EAAE,QAAQ,CAAC;AACzB,iBAAW,UAAU;AACrB,iBAAW,UAAU;AACrB,kBAAY,UAAU;AACtB,aAAO,UAAU,MAAM;AACvB,aAAO,UAAU,MAAM;AACvB,YAAM,UAAU,MAAM;AACtB,YAAM,UAAU,MAAM;AACtB,uBAAiB,UAAU;AAG3B,qBAAe,UAAU,WAAW,MAAM;AACxC,oBAAY,UAAU;AACtB,yBAAiB,IAAI;AAAA,MACvB,GAAG,GAAG;AAEN,YAAM,kBAAkB,CAACA,OAAkB;AACzC,YAAI,CAAC,WAAW,QAAS;AAEzB,cAAMC,SAAQD,GAAE,QAAQ,CAAC;AAGzB,YAAI,CAAC,WAAW,SAAS;AACvB,gBAAM,cAAcC,OAAM,UAAU,OAAO;AAC3C,gBAAM,cAAcA,OAAM,UAAU,OAAO;AAG3C,cAAI,YAAY,SAAS;AACvB,kBAAMC,UAASD,OAAM,UAAU,MAAM;AACrC,kBAAME,UAASF,OAAM,UAAU,MAAM;AACrC,gBAAI,KAAK,IAAIC,OAAM,IAAI,KAAK,KAAK,IAAIC,OAAM,IAAI,GAAG;AAChD,yBAAW,UAAU;AACrB,cAAAH,GAAE,eAAe;AACjB,oBAAM,YAAaE,UAASC,UAAU,IAAI,IAAI;AAC9C,yBAAW,QAAQ,SAAS;AAC5B,oBAAM,UAAUF,OAAM;AACtB,oBAAM,UAAUA,OAAM;AAEtB,+BAAiB,UAAU,CAAC,YAAY;AACxC;AAAA,YACF;AAAA,UACF;AAEA,cAAI,KAAK,IAAI,WAAW,IAAI,kBAAkB,KAAK,IAAI,WAAW,IAAI,gBAAgB;AAEpF,gBAAI,eAAe,SAAS;AAC1B,2BAAa,eAAe,OAAO;AACnC,6BAAe,UAAU;AAAA,YAC3B;AACA,uBAAW,UAAU;AACrB,wBAAY,UAAU;AACtB,6BAAiB,IAAI;AACrB,YAAAD,GAAE,eAAe;AAEjB,kBAAM,UAAUC,OAAM;AACtB,kBAAM,UAAUA,OAAM;AACtB,6BAAiB,UAAU;AAAA,UAC7B;AAEA,gBAAM,UAAUA,OAAM;AACtB,gBAAM,UAAUA,OAAM;AACtB;AAAA,QACF;AAEA,QAAAD,GAAE,eAAe;AACjB,cAAM,SAASC,OAAM,UAAU,MAAM;AACrC,cAAM,SAASA,OAAM,UAAU,MAAM;AAGrC,cAAM,cAAc,SAAS,UAAU;AACvC,yBAAiB,WAAW;AAG5B,cAAM,YAAY,YAAY,UAAU,IAAI;AAC5C,YAAI,KAAK,IAAI,iBAAiB,OAAO,KAAK,WAAW;AACnD,gBAAM,aAAa,KAAK,MAAM,iBAAiB,OAAO;AACtD,2BAAiB,WAAW;AAC5B,qBAAW,QAAQ,UAAU;AAC7B,sBAAY,UAAU;AAAA,QACxB;AACA,cAAM,UAAUA,OAAM;AACtB,cAAM,UAAUA,OAAM;AAAA,MACxB;AAEA,YAAM,iBAAiB,MAAM;AAC3B,mBAAW,UAAU;AACrB,oBAAY,UAAU;AACtB,YAAI,eAAe,SAAS;AAC1B,uBAAa,eAAe,OAAO;AACnC,yBAAe,UAAU;AAAA,QAC3B;AACA,yBAAiB,KAAK;AACtB,iBAAS,oBAAoB,aAAa,eAAe;AACzD,iBAAS,oBAAoB,YAAY,cAAc;AAAA,MACzD;AAEA,eAAS,iBAAiB,aAAa,iBAAiB,EAAE,SAAS,MAAM,CAAC;AAC1E,eAAS,iBAAiB,YAAY,cAAc;AAAA,IACtD;AAAA,IACA,CAAC,UAAU,aAAa,SAAS;AAAA,EACnC;AAEA,QAAM,eAAe,cAAc,YAAY,KAAK,IAAI,OAAO,KAAK;AAEpE,QAAM,kBAAc,0BAAY,MAAM;AACpC,QAAI,YAAY,WAAW,QAAS;AACpC,uBAAmB,UAAU;AAC7B,iBAAa,YAAY;AACzB,iBAAa,IAAI;AAAA,EACnB,GAAG,CAAC,UAAU,YAAY,CAAC;AAE3B,QAAM,uBAAmB;AAAA,IACvB,CAAC,MAA2B;AAC1B,UAAI,SAAU;AACd,UAAI,UAAU,KAAK,EAAE,GAAG,GAAG;AACzB,UAAE,eAAe;AACjB,2BAAmB,UAAU;AAC7B,qBAAa,EAAE,GAAG;AAClB,qBAAa,IAAI;AAAA,MACnB;AAAA,IACF;AAAA,IACA,CAAC,QAAQ;AAAA,EACX;AAEA,QAAM,iBAAa,0BAAY,MAAM;AACnC,UAAM,SAAS,SAAS,WAAW,EAAE;AACrC,QAAI,CAAC,MAAM,MAAM,GAAG;AAClB,YAAM,UAAU,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,MAAM,CAAC;AACnD,YAAM,OAAO;AAAA,IACf;AACA,iBAAa,KAAK;AAAA,EACpB,GAAG,CAAC,WAAW,KAAK,KAAK,KAAK,CAAC;AAE/B,QAAM,oBAAgB;AAAA,IACpB,CAAC,MAA2B;AAC1B,UAAI,EAAE,QAAQ,SAAS;AACrB,mBAAW;AAAA,MACb,WAAW,EAAE,QAAQ,UAAU;AAC7B,qBAAa,KAAK;AAAA,MACpB;AAAA,IACF;AAAA,IACA,CAAC,UAAU;AAAA,EACb;AAEA,QAAM,uBAAmB,0BAAY,CAAC,MAA0C;AAC9E,QAAI,CAAC,mBAAmB,SAAS;AAC/B,QAAE,OAAO,OAAO;AAAA,IAClB;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,MAAI,WAAW;AACb,WACE;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,WAAU;AAAA,QACV,OAAO;AAAA,QACP,UAAU,CAAC,MAAM,aAAa,EAAE,OAAO,MAAM,QAAQ,OAAO,EAAE,EAAE,MAAM,EAAE,CAAC;AAAA,QACzE,SAAS;AAAA,QACT,QAAQ;AAAA,QACR,WAAW;AAAA,QACX,WAAW,4CAA4C,aAAa,EAAE;AAAA,QACtE,WAAS;AAAA;AAAA,IACX;AAAA,EAEJ;AAEA,SACE,6CAAC,SAAI,WAAU,2BACZ;AAAA,qBACC,4CAAC,SAAI,WAAU,uBAAsB,eAAY,QAC9C,wBACH;AAAA,IAEF;AAAA,MAAC;AAAA;AAAA,QACC,aAAa;AAAA,QACb,cAAc;AAAA,QACd,SAAS;AAAA,QACT,WAAW;AAAA,QACX,WAAW,mBAAmB,aAAa,EAAE,IAAI,WAAW,8BAA8B,EAAE;AAAA,QAC5F,MAAK;AAAA,QACL,iBAAe;AAAA,QACf,iBAAe;AAAA,QACf,UAAU,WAAW,KAAK;AAAA,QAEzB;AAAA;AAAA,IACH;AAAA,KACF;AAEJ;AAEA,SAAS,UAAU,MAAkD;AACnE,QAAM,CAAC,GAAG,CAAC,IAAI,KAAK,MAAM,GAAG,EAAE,IAAI,MAAM;AACzC,SAAO,EAAE,OAAO,KAAK,GAAG,SAAS,KAAK,EAAE;AAC1C;AAEA,SAAS,WAAW,OAAe,SAAyB;AAC1D,SAAO,GAAG,KAAK,IAAI,OAAO,OAAO,EAAE,SAAS,GAAG,GAAG,CAAC;AACrD;AAEA,SAAS,kBAAkB,WAA2B;AACpD,SAAO,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,KAAK,IAAI,SAAS,CAAC;AACtD;AAEO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,aAAa;AAAA,EACb,kBAAkB;AAAA,EAClB,YAAY;AACd,GAAoB;AAClB,QAAM,aAAa,YAAY;AAC/B,QAAM,EAAE,OAAO,QAAQ,IAAI,UAAU,KAAK;AAC1C,QAAM,eAAe,QAAQ,KAAK;AAElC,QAAM,qBAAqB,CAAC,MAA2C;AACrE,QAAI,SAAU;AACd,UAAM,OAAO,kBAAkB,SAAS,EAAE,OAAO,KAAK,CAAC;AACvD,UAAM,IAAI,KAAK,MAAM,OAAO,EAAE;AAC9B,UAAM,IAAI,OAAO;AACjB,aAAS,WAAW,GAAG,CAAC,CAAC;AAAA,EAC3B;AAEA,QAAM,uBAAmB;AAAA,IACvB,CAAC,UAAkB;AACjB,YAAM,WAAW,KAAK,IAAI,GAAG,KAAK,IAAI,IAAI,QAAQ,KAAK,CAAC;AACxD,eAAS,WAAW,UAAU,OAAO,CAAC;AAAA,IACxC;AAAA,IACA,CAAC,OAAO,SAAS,QAAQ;AAAA,EAC3B;AAEA,QAAM,qBAAiB;AAAA,IACrB,CAAC,aAAqB;AACpB,eAAS,WAAW,UAAU,OAAO,CAAC;AAAA,IACxC;AAAA,IACA,CAAC,SAAS,QAAQ;AAAA,EACpB;AAEA,QAAM,yBAAqB;AAAA,IACzB,CAAC,UAAkB;AACjB,YAAM,kBAAkB,kBAAkB,eAAe,KAAK;AAC9D,YAAM,IAAI,KAAK,MAAM,kBAAkB,EAAE;AACzC,YAAM,IAAI,kBAAkB;AAC5B,eAAS,WAAW,GAAG,CAAC,CAAC;AAAA,IAC3B;AAAA,IACA,CAAC,cAAc,QAAQ;AAAA,EACzB;AAEA,QAAM,uBAAmB;AAAA,IACvB,CAAC,eAAuB;AACtB,eAAS,WAAW,OAAO,UAAU,CAAC;AAAA,IACxC;AAAA,IACA,CAAC,OAAO,QAAQ;AAAA,EAClB;AAEA,SACE,6CAAC,SAAI,WAAW,aAAa,aAAa,EAAE,IAAI,WAAW,wBAAwB,EAAE,IAClF;AAAA,aAAS,4CAAC,WAAM,WAAU,mBAAmB,iBAAM;AAAA,IAEpD,6CAAC,SAAI,WAAU,uBACb;AAAA,mDAAC,SAAI,WAAU,qBACb;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,OAAO;AAAA,YACP,SAAS;AAAA,YACT,OAAO;AAAA,YACP;AAAA,YACA,aAAa,kBAAkB;AAAA,YAC/B,KAAK;AAAA,YACL,KAAK;AAAA,YACL,WAAU;AAAA;AAAA,QACZ;AAAA,QACA,4CAAC,UAAK,WAAU,uBAAsB,eAAC;AAAA,QACvC;AAAA,UAAC;AAAA;AAAA,YACC,OAAO;AAAA,YACP,SAAS;AAAA,YACT,OAAO;AAAA,YACP,aAAa,CAAC,MAAM,OAAO,CAAC,EAAE,SAAS,GAAG,GAAG;AAAA,YAC7C;AAAA,YACA,aAAa;AAAA,YACb,KAAK;AAAA,YACL,KAAK;AAAA,YACL,WAAU;AAAA;AAAA,QACZ;AAAA,SACF;AAAA,MAEA,6CAAC,SAAI,WAAU,8BACb;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,KAAK;AAAA,YACL,KAAK,KAAK,KAAK;AAAA,YACf,MAAM;AAAA,YACN,OAAO;AAAA,YACP,UAAU;AAAA,YACV;AAAA,YACA,UAAU;AAAA,YACV,WAAU;AAAA,YACV,cAAW;AAAA;AAAA,QACb;AAAA,QACA,4CAAC,SAAI,WAAU,2BACZ,gBAAM,KAAK,EAAE,QAAQ,WAAW,GAAG,CAAC,GAAG,MAAM;AAC5C,gBAAM,OAAO,KAAK,MAAO,KAAK,YAAa,CAAC;AAC5C,gBAAM,UAAW,IAAI,YAAa;AAClC,iBACE,4CAAC,UAAa,OAAO,EAAE,MAAM,GAAG,OAAO,IAAI,GACxC,kBADQ,CAEX;AAAA,QAEJ,CAAC,GACH;AAAA,SACF;AAAA,OACF;AAAA,KACF;AAEJ;","names":["e","touch","deltaX","deltaY"]}
package/dist/index.js CHANGED
@@ -15,8 +15,13 @@ function DraggableValue({
15
15
  }) {
16
16
  const [isEditing, setIsEditing] = useState(false);
17
17
  const [editValue, setEditValue] = useState("");
18
+ const [showMagnifier, setShowMagnifier] = useState(false);
19
+ const enteredViaKeyboard = useRef(false);
18
20
  const isDragging = useRef(false);
19
21
  const hasDragged = useRef(false);
22
+ const isLongPress = useRef(false);
23
+ const needsBuffer = useRef(false);
24
+ const longPressTimer = useRef(null);
20
25
  const startX = useRef(0);
21
26
  const startY = useRef(0);
22
27
  const lastX = useRef(0);
@@ -68,34 +73,76 @@ function DraggableValue({
68
73
  const touch = e.touches[0];
69
74
  isDragging.current = true;
70
75
  hasDragged.current = false;
76
+ isLongPress.current = false;
71
77
  startX.current = touch.clientX;
72
78
  startY.current = touch.clientY;
73
79
  lastX.current = touch.clientX;
74
80
  lastY.current = touch.clientY;
75
81
  accumulatedDelta.current = 0;
82
+ longPressTimer.current = setTimeout(() => {
83
+ isLongPress.current = true;
84
+ setShowMagnifier(true);
85
+ }, 300);
76
86
  const handleTouchMove = (e2) => {
77
87
  if (!isDragging.current) return;
78
88
  const touch2 = e2.touches[0];
79
- const totalDeltaX = Math.abs(touch2.clientX - startX.current);
80
- const totalDeltaY = Math.abs(touch2.clientY - startY.current);
81
- if (totalDeltaX > DRAG_THRESHOLD || totalDeltaY > DRAG_THRESHOLD) {
82
- hasDragged.current = true;
83
- e2.preventDefault();
89
+ if (!hasDragged.current) {
90
+ const totalDeltaX = touch2.clientX - startX.current;
91
+ const totalDeltaY = touch2.clientY - startY.current;
92
+ if (isLongPress.current) {
93
+ const deltaX2 = touch2.clientX - lastX.current;
94
+ const deltaY2 = touch2.clientY - lastY.current;
95
+ if (Math.abs(deltaX2) > 0 || Math.abs(deltaY2) > 0) {
96
+ hasDragged.current = true;
97
+ e2.preventDefault();
98
+ const direction = deltaX2 + deltaY2 > 0 ? 1 : -1;
99
+ onDeltaRef.current(direction);
100
+ lastX.current = touch2.clientX;
101
+ lastY.current = touch2.clientY;
102
+ accumulatedDelta.current = -direction * 0.8;
103
+ return;
104
+ }
105
+ }
106
+ if (Math.abs(totalDeltaX) > DRAG_THRESHOLD || Math.abs(totalDeltaY) > DRAG_THRESHOLD) {
107
+ if (longPressTimer.current) {
108
+ clearTimeout(longPressTimer.current);
109
+ longPressTimer.current = null;
110
+ }
111
+ hasDragged.current = true;
112
+ needsBuffer.current = true;
113
+ setShowMagnifier(true);
114
+ e2.preventDefault();
115
+ lastX.current = touch2.clientX;
116
+ lastY.current = touch2.clientY;
117
+ accumulatedDelta.current = 0;
118
+ }
119
+ lastX.current = touch2.clientX;
120
+ lastY.current = touch2.clientY;
121
+ return;
84
122
  }
123
+ e2.preventDefault();
85
124
  const deltaX = touch2.clientX - lastX.current;
86
125
  const deltaY = touch2.clientY - lastY.current;
87
- const deltaValue = (deltaX - deltaY) / sensitivity;
126
+ const deltaValue = (deltaX + deltaY) / sensitivity;
88
127
  accumulatedDelta.current += deltaValue;
89
- const wholeDelta = Math.trunc(accumulatedDelta.current);
90
- if (wholeDelta !== 0) {
128
+ const threshold = needsBuffer.current ? 2 : 1;
129
+ if (Math.abs(accumulatedDelta.current) >= threshold) {
130
+ const wholeDelta = Math.trunc(accumulatedDelta.current);
91
131
  accumulatedDelta.current -= wholeDelta;
92
132
  onDeltaRef.current(wholeDelta);
133
+ needsBuffer.current = false;
93
134
  }
94
135
  lastX.current = touch2.clientX;
95
136
  lastY.current = touch2.clientY;
96
137
  };
97
138
  const handleTouchEnd = () => {
98
139
  isDragging.current = false;
140
+ needsBuffer.current = false;
141
+ if (longPressTimer.current) {
142
+ clearTimeout(longPressTimer.current);
143
+ longPressTimer.current = null;
144
+ }
145
+ setShowMagnifier(false);
99
146
  document.removeEventListener("touchmove", handleTouchMove);
100
147
  document.removeEventListener("touchend", handleTouchEnd);
101
148
  };
@@ -107,6 +154,7 @@ function DraggableValue({
107
154
  const displayValue = formatValue ? formatValue(value) : String(value);
108
155
  const handleClick = useCallback(() => {
109
156
  if (disabled || hasDragged.current) return;
157
+ enteredViaKeyboard.current = false;
110
158
  setEditValue(displayValue);
111
159
  setIsEditing(true);
112
160
  }, [disabled, displayValue]);
@@ -115,6 +163,7 @@ function DraggableValue({
115
163
  if (disabled) return;
116
164
  if (/^[0-9]$/.test(e.key)) {
117
165
  e.preventDefault();
166
+ enteredViaKeyboard.current = true;
118
167
  setEditValue(e.key);
119
168
  setIsEditing(true);
120
169
  }
@@ -139,6 +188,11 @@ function DraggableValue({
139
188
  },
140
189
  [commitEdit]
141
190
  );
191
+ const handleInputFocus = useCallback((e) => {
192
+ if (!enteredViaKeyboard.current) {
193
+ e.target.select();
194
+ }
195
+ }, []);
142
196
  if (isEditing) {
143
197
  return /* @__PURE__ */ jsx(
144
198
  "input",
@@ -147,7 +201,7 @@ function DraggableValue({
147
201
  inputMode: "numeric",
148
202
  value: editValue,
149
203
  onChange: (e) => setEditValue(e.target.value.replace(/\D/g, "").slice(-2)),
150
- onFocus: (e) => e.target.select(),
204
+ onFocus: handleInputFocus,
151
205
  onBlur: commitEdit,
152
206
  onKeyDown: handleKeyDown,
153
207
  className: `scrubtime-value scrubtime-value--editing ${className || ""}`,
@@ -155,21 +209,24 @@ function DraggableValue({
155
209
  }
156
210
  );
157
211
  }
158
- return /* @__PURE__ */ jsx(
159
- "div",
160
- {
161
- onMouseDown: handleMouseDown,
162
- onTouchStart: handleTouchStart,
163
- onClick: handleClick,
164
- onKeyDown: handleDivKeyDown,
165
- className: `scrubtime-value ${className || ""} ${disabled ? "scrubtime-value--disabled" : ""}`,
166
- role: "spinbutton",
167
- "aria-valuenow": value,
168
- "aria-disabled": disabled,
169
- tabIndex: disabled ? -1 : 0,
170
- children: displayValue
171
- }
172
- );
212
+ return /* @__PURE__ */ jsxs("div", { className: "scrubtime-value-wrapper", children: [
213
+ showMagnifier && /* @__PURE__ */ jsx("div", { className: "scrubtime-magnifier", "aria-hidden": "true", children: displayValue }),
214
+ /* @__PURE__ */ jsx(
215
+ "div",
216
+ {
217
+ onMouseDown: handleMouseDown,
218
+ onTouchStart: handleTouchStart,
219
+ onClick: handleClick,
220
+ onKeyDown: handleDivKeyDown,
221
+ className: `scrubtime-value ${className || ""} ${disabled ? "scrubtime-value--disabled" : ""}`,
222
+ role: "spinbutton",
223
+ "aria-valuenow": value,
224
+ "aria-disabled": disabled,
225
+ tabIndex: disabled ? -1 : 0,
226
+ children: displayValue
227
+ }
228
+ )
229
+ ] });
173
230
  }
174
231
  function parseTime(time) {
175
232
  const [h, m] = time.split(":").map(Number);
@@ -255,7 +312,7 @@ function TimePicker({
255
312
  onSet: handleMinutesSet,
256
313
  formatValue: (v) => String(v).padStart(2, "0"),
257
314
  disabled,
258
- sensitivity: dragSensitivity / 2,
315
+ sensitivity: dragSensitivity,
259
316
  min: 0,
260
317
  max: 59,
261
318
  className: "scrubtime-minutes"
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/TimePicker.tsx"],"sourcesContent":["import { useRef, useCallback, useState } from 'react';\n\nexport interface TimePickerProps {\n /** Current time value in \"H:mm\" or \"HH:mm\" format */\n value: string;\n /** Callback fired when time changes */\n onChange: (value: string) => void;\n /** Optional label displayed above the picker */\n label?: string;\n /** Custom class name for the root element */\n className?: string;\n /** Disable the picker */\n disabled?: boolean;\n /** Slider step in minutes (default: 15) */\n sliderStep?: number;\n /** Drag sensitivity - pixels per unit change (default: 3) */\n dragSensitivity?: number;\n /** Number of equal parts to divide the 24h range into (default: 4 = labels at 0,6,12,18,24) */\n divisions?: number;\n}\n\ninterface DraggableValueProps {\n value: number;\n onDelta: (delta: number) => void;\n onSet: (value: number) => void;\n formatValue?: (v: number) => string;\n className?: string;\n disabled?: boolean;\n sensitivity: number;\n min: number;\n max: number;\n}\n\nconst DRAG_THRESHOLD = 3;\n\nfunction DraggableValue({\n value,\n onDelta,\n onSet,\n formatValue,\n className,\n disabled,\n sensitivity,\n min,\n max,\n}: DraggableValueProps) {\n const [isEditing, setIsEditing] = useState(false);\n const [editValue, setEditValue] = useState('');\n const isDragging = useRef(false);\n const hasDragged = useRef(false);\n const startX = useRef(0);\n const startY = useRef(0);\n const lastX = useRef(0);\n const lastY = useRef(0);\n const accumulatedDelta = useRef(0);\n const onDeltaRef = useRef(onDelta);\n onDeltaRef.current = onDelta;\n\n const handleMouseDown = useCallback(\n (e: React.MouseEvent) => {\n if (disabled || isEditing) return;\n\n isDragging.current = true;\n hasDragged.current = false;\n startX.current = e.clientX;\n lastX.current = e.clientX;\n accumulatedDelta.current = 0;\n document.body.style.cursor = 'ew-resize';\n document.body.style.userSelect = 'none';\n\n const handleMouseMove = (e: MouseEvent) => {\n if (!isDragging.current) return;\n\n const totalDeltaX = Math.abs(e.clientX - startX.current);\n if (totalDeltaX > DRAG_THRESHOLD) {\n hasDragged.current = true;\n }\n\n const deltaX = e.clientX - lastX.current;\n const deltaValue = deltaX / sensitivity;\n accumulatedDelta.current += deltaValue;\n\n const wholeDelta = Math.trunc(accumulatedDelta.current);\n if (wholeDelta !== 0) {\n accumulatedDelta.current -= wholeDelta;\n onDeltaRef.current(wholeDelta);\n }\n lastX.current = e.clientX;\n };\n\n const handleMouseUp = () => {\n isDragging.current = false;\n document.body.style.cursor = '';\n document.body.style.userSelect = '';\n document.removeEventListener('mousemove', handleMouseMove);\n document.removeEventListener('mouseup', handleMouseUp);\n };\n\n document.addEventListener('mousemove', handleMouseMove);\n document.addEventListener('mouseup', handleMouseUp);\n },\n [disabled, sensitivity, isEditing]\n );\n\n const handleTouchStart = useCallback(\n (e: React.TouchEvent) => {\n if (disabled || isEditing) return;\n\n const touch = e.touches[0];\n isDragging.current = true;\n hasDragged.current = false;\n startX.current = touch.clientX;\n startY.current = touch.clientY;\n lastX.current = touch.clientX;\n lastY.current = touch.clientY;\n accumulatedDelta.current = 0;\n\n const handleTouchMove = (e: TouchEvent) => {\n if (!isDragging.current) return;\n\n const touch = e.touches[0];\n const totalDeltaX = Math.abs(touch.clientX - startX.current);\n const totalDeltaY = Math.abs(touch.clientY - startY.current);\n if (totalDeltaX > DRAG_THRESHOLD || totalDeltaY > DRAG_THRESHOLD) {\n hasDragged.current = true;\n e.preventDefault(); // Prevent scrolling once dragging starts\n }\n\n const deltaX = touch.clientX - lastX.current;\n const deltaY = touch.clientY - lastY.current;\n // Horizontal: right = increase, left = decrease\n // Vertical: up = increase, down = decrease\n const deltaValue = (deltaX - deltaY) / sensitivity;\n accumulatedDelta.current += deltaValue;\n\n const wholeDelta = Math.trunc(accumulatedDelta.current);\n if (wholeDelta !== 0) {\n accumulatedDelta.current -= wholeDelta;\n onDeltaRef.current(wholeDelta);\n }\n lastX.current = touch.clientX;\n lastY.current = touch.clientY;\n };\n\n const handleTouchEnd = () => {\n isDragging.current = false;\n document.removeEventListener('touchmove', handleTouchMove);\n document.removeEventListener('touchend', handleTouchEnd);\n };\n\n document.addEventListener('touchmove', handleTouchMove, { passive: false });\n document.addEventListener('touchend', handleTouchEnd);\n },\n [disabled, sensitivity, isEditing]\n );\n\n const displayValue = formatValue ? formatValue(value) : String(value);\n\n const handleClick = useCallback(() => {\n if (disabled || hasDragged.current) return;\n setEditValue(displayValue);\n setIsEditing(true);\n }, [disabled, displayValue]);\n\n const handleDivKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (disabled) return;\n if (/^[0-9]$/.test(e.key)) {\n e.preventDefault();\n setEditValue(e.key);\n setIsEditing(true);\n }\n },\n [disabled]\n );\n\n const commitEdit = useCallback(() => {\n const parsed = parseInt(editValue, 10);\n if (!isNaN(parsed)) {\n const clamped = Math.max(min, Math.min(max, parsed));\n onSet(clamped);\n }\n setIsEditing(false);\n }, [editValue, min, max, onSet]);\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (e.key === 'Enter') {\n commitEdit();\n } else if (e.key === 'Escape') {\n setIsEditing(false);\n }\n },\n [commitEdit]\n );\n\n if (isEditing) {\n return (\n <input\n type=\"text\"\n inputMode=\"numeric\"\n value={editValue}\n onChange={(e) => setEditValue(e.target.value.replace(/\\D/g, '').slice(-2))}\n onFocus={(e) => e.target.select()}\n onBlur={commitEdit}\n onKeyDown={handleKeyDown}\n className={`scrubtime-value scrubtime-value--editing ${className || ''}`}\n autoFocus\n />\n );\n }\n\n return (\n <div\n onMouseDown={handleMouseDown}\n onTouchStart={handleTouchStart}\n onClick={handleClick}\n onKeyDown={handleDivKeyDown}\n className={`scrubtime-value ${className || ''} ${disabled ? 'scrubtime-value--disabled' : ''}`}\n role=\"spinbutton\"\n aria-valuenow={value}\n aria-disabled={disabled}\n tabIndex={disabled ? -1 : 0}\n >\n {displayValue}\n </div>\n );\n}\n\nfunction parseTime(time: string): { hours: number; minutes: number } {\n const [h, m] = time.split(':').map(Number);\n return { hours: h || 0, minutes: m || 0 };\n}\n\nfunction formatTime(hours: number, minutes: number): string {\n return `${hours}:${String(minutes).padStart(2, '0')}`;\n}\n\nfunction clampTotalMinutes(totalMins: number): number {\n return Math.max(0, Math.min(23 * 60 + 59, totalMins));\n}\n\nexport function TimePicker({\n value,\n onChange,\n label,\n className,\n disabled = false,\n sliderStep = 15,\n dragSensitivity = 3,\n divisions = 4,\n}: TimePickerProps) {\n const labelCount = divisions + 1;\n const { hours, minutes } = parseTime(value);\n const totalMinutes = hours * 60 + minutes;\n\n const handleSliderChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n if (disabled) return;\n const mins = clampTotalMinutes(parseInt(e.target.value));\n const h = Math.floor(mins / 60);\n const m = mins % 60;\n onChange(formatTime(h, m));\n };\n\n const handleHoursDelta = useCallback(\n (delta: number) => {\n const newHours = Math.max(0, Math.min(23, hours + delta));\n onChange(formatTime(newHours, minutes));\n },\n [hours, minutes, onChange]\n );\n\n const handleHoursSet = useCallback(\n (newHours: number) => {\n onChange(formatTime(newHours, minutes));\n },\n [minutes, onChange]\n );\n\n const handleMinutesDelta = useCallback(\n (delta: number) => {\n const newTotalMinutes = clampTotalMinutes(totalMinutes + delta);\n const h = Math.floor(newTotalMinutes / 60);\n const m = newTotalMinutes % 60;\n onChange(formatTime(h, m));\n },\n [totalMinutes, onChange]\n );\n\n const handleMinutesSet = useCallback(\n (newMinutes: number) => {\n onChange(formatTime(hours, newMinutes));\n },\n [hours, onChange]\n );\n\n return (\n <div className={`scrubtime ${className || ''} ${disabled ? 'scrubtime--disabled' : ''}`}>\n {label && <label className=\"scrubtime-label\">{label}</label>}\n\n <div className=\"scrubtime-container\">\n <div className=\"scrubtime-display\">\n <DraggableValue\n value={hours}\n onDelta={handleHoursDelta}\n onSet={handleHoursSet}\n disabled={disabled}\n sensitivity={dragSensitivity * 2}\n min={0}\n max={23}\n className=\"scrubtime-hours\"\n />\n <span className=\"scrubtime-separator\">:</span>\n <DraggableValue\n value={minutes}\n onDelta={handleMinutesDelta}\n onSet={handleMinutesSet}\n formatValue={(v) => String(v).padStart(2, '0')}\n disabled={disabled}\n sensitivity={dragSensitivity / 2}\n min={0}\n max={59}\n className=\"scrubtime-minutes\"\n />\n </div>\n\n <div className=\"scrubtime-slider-container\">\n <input\n type=\"range\"\n min={0}\n max={23 * 60 + 59}\n step={sliderStep}\n value={totalMinutes}\n onChange={handleSliderChange}\n disabled={disabled}\n tabIndex={-1}\n className=\"scrubtime-slider\"\n aria-label=\"Time slider\"\n />\n <div className=\"scrubtime-slider-labels\">\n {Array.from({ length: labelCount }, (_, i) => {\n const hour = Math.round((24 / divisions) * i);\n const percent = (i / divisions) * 100;\n return (\n <span key={i} style={{ left: `${percent}%` }}>\n {hour}\n </span>\n );\n })}\n </div>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":["e","touch"]}
1
+ {"version":3,"sources":["../src/TimePicker.tsx"],"sourcesContent":["import { useRef, useCallback, useState } from 'react';\n\nexport interface TimePickerProps {\n /** Current time value in \"H:mm\" or \"HH:mm\" format */\n value: string;\n /** Callback fired when time changes */\n onChange: (value: string) => void;\n /** Optional label displayed above the picker */\n label?: string;\n /** Custom class name for the root element */\n className?: string;\n /** Disable the picker */\n disabled?: boolean;\n /** Slider step in minutes (default: 15) */\n sliderStep?: number;\n /** Drag sensitivity - pixels per unit change (default: 3) */\n dragSensitivity?: number;\n /** Number of equal parts to divide the 24h range into (default: 4 = labels at 0,6,12,18,24) */\n divisions?: number;\n}\n\ninterface DraggableValueProps {\n value: number;\n onDelta: (delta: number) => void;\n onSet: (value: number) => void;\n formatValue?: (v: number) => string;\n className?: string;\n disabled?: boolean;\n sensitivity: number;\n min: number;\n max: number;\n}\n\nconst DRAG_THRESHOLD = 3;\n\nfunction DraggableValue({\n value,\n onDelta,\n onSet,\n formatValue,\n className,\n disabled,\n sensitivity,\n min,\n max,\n}: DraggableValueProps) {\n const [isEditing, setIsEditing] = useState(false);\n const [editValue, setEditValue] = useState('');\n const [showMagnifier, setShowMagnifier] = useState(false);\n const enteredViaKeyboard = useRef(false);\n const isDragging = useRef(false);\n const hasDragged = useRef(false);\n const isLongPress = useRef(false);\n const needsBuffer = useRef(false);\n const longPressTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n const startX = useRef(0);\n const startY = useRef(0);\n const lastX = useRef(0);\n const lastY = useRef(0);\n const accumulatedDelta = useRef(0);\n const onDeltaRef = useRef(onDelta);\n onDeltaRef.current = onDelta;\n\n const handleMouseDown = useCallback(\n (e: React.MouseEvent) => {\n if (disabled || isEditing) return;\n\n isDragging.current = true;\n hasDragged.current = false;\n startX.current = e.clientX;\n lastX.current = e.clientX;\n accumulatedDelta.current = 0;\n document.body.style.cursor = 'ew-resize';\n document.body.style.userSelect = 'none';\n\n const handleMouseMove = (e: MouseEvent) => {\n if (!isDragging.current) return;\n\n const totalDeltaX = Math.abs(e.clientX - startX.current);\n if (totalDeltaX > DRAG_THRESHOLD) {\n hasDragged.current = true;\n }\n\n const deltaX = e.clientX - lastX.current;\n const deltaValue = deltaX / sensitivity;\n accumulatedDelta.current += deltaValue;\n\n const wholeDelta = Math.trunc(accumulatedDelta.current);\n if (wholeDelta !== 0) {\n accumulatedDelta.current -= wholeDelta;\n onDeltaRef.current(wholeDelta);\n }\n lastX.current = e.clientX;\n };\n\n const handleMouseUp = () => {\n isDragging.current = false;\n document.body.style.cursor = '';\n document.body.style.userSelect = '';\n document.removeEventListener('mousemove', handleMouseMove);\n document.removeEventListener('mouseup', handleMouseUp);\n };\n\n document.addEventListener('mousemove', handleMouseMove);\n document.addEventListener('mouseup', handleMouseUp);\n },\n [disabled, sensitivity, isEditing]\n );\n\n const handleTouchStart = useCallback(\n (e: React.TouchEvent) => {\n if (disabled || isEditing) return;\n\n const touch = e.touches[0];\n isDragging.current = true;\n hasDragged.current = false;\n isLongPress.current = false;\n startX.current = touch.clientX;\n startY.current = touch.clientY;\n lastX.current = touch.clientX;\n lastY.current = touch.clientY;\n accumulatedDelta.current = 0;\n\n // Long press timer - activates after 300ms, no threshold needed after\n longPressTimer.current = setTimeout(() => {\n isLongPress.current = true;\n setShowMagnifier(true);\n }, 300);\n\n const handleTouchMove = (e: TouchEvent) => {\n if (!isDragging.current) return;\n\n const touch = e.touches[0];\n\n // Before drag is activated, check threshold or long press\n if (!hasDragged.current) {\n const totalDeltaX = touch.clientX - startX.current;\n const totalDeltaY = touch.clientY - startY.current;\n\n // Long press activated - start immediately with +1/-1 on any movement\n if (isLongPress.current) {\n const deltaX = touch.clientX - lastX.current;\n const deltaY = touch.clientY - lastY.current;\n if (Math.abs(deltaX) > 0 || Math.abs(deltaY) > 0) {\n hasDragged.current = true;\n e.preventDefault();\n const direction = (deltaX + deltaY) > 0 ? 1 : -1;\n onDeltaRef.current(direction);\n lastX.current = touch.clientX;\n lastY.current = touch.clientY;\n // Add buffer to prevent rapid-fire changes after activation\n accumulatedDelta.current = -direction * 0.8;\n return;\n }\n }\n\n if (Math.abs(totalDeltaX) > DRAG_THRESHOLD || Math.abs(totalDeltaY) > DRAG_THRESHOLD) {\n // Cancel long press timer since drag started\n if (longPressTimer.current) {\n clearTimeout(longPressTimer.current);\n longPressTimer.current = null;\n }\n hasDragged.current = true;\n needsBuffer.current = true;\n setShowMagnifier(true);\n e.preventDefault();\n // Show magnifier first, don't change value yet - further dragging will mutate\n lastX.current = touch.clientX;\n lastY.current = touch.clientY;\n accumulatedDelta.current = 0;\n }\n // Always update lastX/lastY so long press activation has fresh position\n lastX.current = touch.clientX;\n lastY.current = touch.clientY;\n return;\n }\n\n e.preventDefault();\n const deltaX = touch.clientX - lastX.current;\n const deltaY = touch.clientY - lastY.current;\n // Horizontal: right = increase, left = decrease\n // Vertical: up = increase, down = decrease\n const deltaValue = (deltaX + deltaY) / sensitivity;\n accumulatedDelta.current += deltaValue;\n\n // Require more movement for first change after distance activation\n const threshold = needsBuffer.current ? 2 : 1;\n if (Math.abs(accumulatedDelta.current) >= threshold) {\n const wholeDelta = Math.trunc(accumulatedDelta.current);\n accumulatedDelta.current -= wholeDelta;\n onDeltaRef.current(wholeDelta);\n needsBuffer.current = false;\n }\n lastX.current = touch.clientX;\n lastY.current = touch.clientY;\n };\n\n const handleTouchEnd = () => {\n isDragging.current = false;\n needsBuffer.current = false;\n if (longPressTimer.current) {\n clearTimeout(longPressTimer.current);\n longPressTimer.current = null;\n }\n setShowMagnifier(false);\n document.removeEventListener('touchmove', handleTouchMove);\n document.removeEventListener('touchend', handleTouchEnd);\n };\n\n document.addEventListener('touchmove', handleTouchMove, { passive: false });\n document.addEventListener('touchend', handleTouchEnd);\n },\n [disabled, sensitivity, isEditing]\n );\n\n const displayValue = formatValue ? formatValue(value) : String(value);\n\n const handleClick = useCallback(() => {\n if (disabled || hasDragged.current) return;\n enteredViaKeyboard.current = false;\n setEditValue(displayValue);\n setIsEditing(true);\n }, [disabled, displayValue]);\n\n const handleDivKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (disabled) return;\n if (/^[0-9]$/.test(e.key)) {\n e.preventDefault();\n enteredViaKeyboard.current = true;\n setEditValue(e.key);\n setIsEditing(true);\n }\n },\n [disabled]\n );\n\n const commitEdit = useCallback(() => {\n const parsed = parseInt(editValue, 10);\n if (!isNaN(parsed)) {\n const clamped = Math.max(min, Math.min(max, parsed));\n onSet(clamped);\n }\n setIsEditing(false);\n }, [editValue, min, max, onSet]);\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (e.key === 'Enter') {\n commitEdit();\n } else if (e.key === 'Escape') {\n setIsEditing(false);\n }\n },\n [commitEdit]\n );\n\n const handleInputFocus = useCallback((e: React.FocusEvent<HTMLInputElement>) => {\n if (!enteredViaKeyboard.current) {\n e.target.select();\n }\n }, []);\n\n if (isEditing) {\n return (\n <input\n type=\"text\"\n inputMode=\"numeric\"\n value={editValue}\n onChange={(e) => setEditValue(e.target.value.replace(/\\D/g, '').slice(-2))}\n onFocus={handleInputFocus}\n onBlur={commitEdit}\n onKeyDown={handleKeyDown}\n className={`scrubtime-value scrubtime-value--editing ${className || ''}`}\n autoFocus\n />\n );\n }\n\n return (\n <div className=\"scrubtime-value-wrapper\">\n {showMagnifier && (\n <div className=\"scrubtime-magnifier\" aria-hidden=\"true\">\n {displayValue}\n </div>\n )}\n <div\n onMouseDown={handleMouseDown}\n onTouchStart={handleTouchStart}\n onClick={handleClick}\n onKeyDown={handleDivKeyDown}\n className={`scrubtime-value ${className || ''} ${disabled ? 'scrubtime-value--disabled' : ''}`}\n role=\"spinbutton\"\n aria-valuenow={value}\n aria-disabled={disabled}\n tabIndex={disabled ? -1 : 0}\n >\n {displayValue}\n </div>\n </div>\n );\n}\n\nfunction parseTime(time: string): { hours: number; minutes: number } {\n const [h, m] = time.split(':').map(Number);\n return { hours: h || 0, minutes: m || 0 };\n}\n\nfunction formatTime(hours: number, minutes: number): string {\n return `${hours}:${String(minutes).padStart(2, '0')}`;\n}\n\nfunction clampTotalMinutes(totalMins: number): number {\n return Math.max(0, Math.min(23 * 60 + 59, totalMins));\n}\n\nexport function TimePicker({\n value,\n onChange,\n label,\n className,\n disabled = false,\n sliderStep = 15,\n dragSensitivity = 3,\n divisions = 4,\n}: TimePickerProps) {\n const labelCount = divisions + 1;\n const { hours, minutes } = parseTime(value);\n const totalMinutes = hours * 60 + minutes;\n\n const handleSliderChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n if (disabled) return;\n const mins = clampTotalMinutes(parseInt(e.target.value));\n const h = Math.floor(mins / 60);\n const m = mins % 60;\n onChange(formatTime(h, m));\n };\n\n const handleHoursDelta = useCallback(\n (delta: number) => {\n const newHours = Math.max(0, Math.min(23, hours + delta));\n onChange(formatTime(newHours, minutes));\n },\n [hours, minutes, onChange]\n );\n\n const handleHoursSet = useCallback(\n (newHours: number) => {\n onChange(formatTime(newHours, minutes));\n },\n [minutes, onChange]\n );\n\n const handleMinutesDelta = useCallback(\n (delta: number) => {\n const newTotalMinutes = clampTotalMinutes(totalMinutes + delta);\n const h = Math.floor(newTotalMinutes / 60);\n const m = newTotalMinutes % 60;\n onChange(formatTime(h, m));\n },\n [totalMinutes, onChange]\n );\n\n const handleMinutesSet = useCallback(\n (newMinutes: number) => {\n onChange(formatTime(hours, newMinutes));\n },\n [hours, onChange]\n );\n\n return (\n <div className={`scrubtime ${className || ''} ${disabled ? 'scrubtime--disabled' : ''}`}>\n {label && <label className=\"scrubtime-label\">{label}</label>}\n\n <div className=\"scrubtime-container\">\n <div className=\"scrubtime-display\">\n <DraggableValue\n value={hours}\n onDelta={handleHoursDelta}\n onSet={handleHoursSet}\n disabled={disabled}\n sensitivity={dragSensitivity * 2}\n min={0}\n max={23}\n className=\"scrubtime-hours\"\n />\n <span className=\"scrubtime-separator\">:</span>\n <DraggableValue\n value={minutes}\n onDelta={handleMinutesDelta}\n onSet={handleMinutesSet}\n formatValue={(v) => String(v).padStart(2, '0')}\n disabled={disabled}\n sensitivity={dragSensitivity}\n min={0}\n max={59}\n className=\"scrubtime-minutes\"\n />\n </div>\n\n <div className=\"scrubtime-slider-container\">\n <input\n type=\"range\"\n min={0}\n max={23 * 60 + 59}\n step={sliderStep}\n value={totalMinutes}\n onChange={handleSliderChange}\n disabled={disabled}\n tabIndex={-1}\n className=\"scrubtime-slider\"\n aria-label=\"Time slider\"\n />\n <div className=\"scrubtime-slider-labels\">\n {Array.from({ length: labelCount }, (_, i) => {\n const hour = Math.round((24 / divisions) * i);\n const percent = (i / divisions) * 100;\n return (\n <span key={i} style={{ left: `${percent}%` }}>\n {hour}\n </span>\n );\n })}\n </div>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":";AAAA,SAAS,QAAQ,aAAa,gBAAgB;AAyQxC,cAeF,YAfE;AAxON,IAAM,iBAAiB;AAEvB,SAAS,eAAe;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAwB;AACtB,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,KAAK;AAChD,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,EAAE;AAC7C,QAAM,CAAC,eAAe,gBAAgB,IAAI,SAAS,KAAK;AACxD,QAAM,qBAAqB,OAAO,KAAK;AACvC,QAAM,aAAa,OAAO,KAAK;AAC/B,QAAM,aAAa,OAAO,KAAK;AAC/B,QAAM,cAAc,OAAO,KAAK;AAChC,QAAM,cAAc,OAAO,KAAK;AAChC,QAAM,iBAAiB,OAA6C,IAAI;AACxE,QAAM,SAAS,OAAO,CAAC;AACvB,QAAM,SAAS,OAAO,CAAC;AACvB,QAAM,QAAQ,OAAO,CAAC;AACtB,QAAM,QAAQ,OAAO,CAAC;AACtB,QAAM,mBAAmB,OAAO,CAAC;AACjC,QAAM,aAAa,OAAO,OAAO;AACjC,aAAW,UAAU;AAErB,QAAM,kBAAkB;AAAA,IACtB,CAAC,MAAwB;AACvB,UAAI,YAAY,UAAW;AAE3B,iBAAW,UAAU;AACrB,iBAAW,UAAU;AACrB,aAAO,UAAU,EAAE;AACnB,YAAM,UAAU,EAAE;AAClB,uBAAiB,UAAU;AAC3B,eAAS,KAAK,MAAM,SAAS;AAC7B,eAAS,KAAK,MAAM,aAAa;AAEjC,YAAM,kBAAkB,CAACA,OAAkB;AACzC,YAAI,CAAC,WAAW,QAAS;AAEzB,cAAM,cAAc,KAAK,IAAIA,GAAE,UAAU,OAAO,OAAO;AACvD,YAAI,cAAc,gBAAgB;AAChC,qBAAW,UAAU;AAAA,QACvB;AAEA,cAAM,SAASA,GAAE,UAAU,MAAM;AACjC,cAAM,aAAa,SAAS;AAC5B,yBAAiB,WAAW;AAE5B,cAAM,aAAa,KAAK,MAAM,iBAAiB,OAAO;AACtD,YAAI,eAAe,GAAG;AACpB,2BAAiB,WAAW;AAC5B,qBAAW,QAAQ,UAAU;AAAA,QAC/B;AACA,cAAM,UAAUA,GAAE;AAAA,MACpB;AAEA,YAAM,gBAAgB,MAAM;AAC1B,mBAAW,UAAU;AACrB,iBAAS,KAAK,MAAM,SAAS;AAC7B,iBAAS,KAAK,MAAM,aAAa;AACjC,iBAAS,oBAAoB,aAAa,eAAe;AACzD,iBAAS,oBAAoB,WAAW,aAAa;AAAA,MACvD;AAEA,eAAS,iBAAiB,aAAa,eAAe;AACtD,eAAS,iBAAiB,WAAW,aAAa;AAAA,IACpD;AAAA,IACA,CAAC,UAAU,aAAa,SAAS;AAAA,EACnC;AAEA,QAAM,mBAAmB;AAAA,IACvB,CAAC,MAAwB;AACvB,UAAI,YAAY,UAAW;AAE3B,YAAM,QAAQ,EAAE,QAAQ,CAAC;AACzB,iBAAW,UAAU;AACrB,iBAAW,UAAU;AACrB,kBAAY,UAAU;AACtB,aAAO,UAAU,MAAM;AACvB,aAAO,UAAU,MAAM;AACvB,YAAM,UAAU,MAAM;AACtB,YAAM,UAAU,MAAM;AACtB,uBAAiB,UAAU;AAG3B,qBAAe,UAAU,WAAW,MAAM;AACxC,oBAAY,UAAU;AACtB,yBAAiB,IAAI;AAAA,MACvB,GAAG,GAAG;AAEN,YAAM,kBAAkB,CAACA,OAAkB;AACzC,YAAI,CAAC,WAAW,QAAS;AAEzB,cAAMC,SAAQD,GAAE,QAAQ,CAAC;AAGzB,YAAI,CAAC,WAAW,SAAS;AACvB,gBAAM,cAAcC,OAAM,UAAU,OAAO;AAC3C,gBAAM,cAAcA,OAAM,UAAU,OAAO;AAG3C,cAAI,YAAY,SAAS;AACvB,kBAAMC,UAASD,OAAM,UAAU,MAAM;AACrC,kBAAME,UAASF,OAAM,UAAU,MAAM;AACrC,gBAAI,KAAK,IAAIC,OAAM,IAAI,KAAK,KAAK,IAAIC,OAAM,IAAI,GAAG;AAChD,yBAAW,UAAU;AACrB,cAAAH,GAAE,eAAe;AACjB,oBAAM,YAAaE,UAASC,UAAU,IAAI,IAAI;AAC9C,yBAAW,QAAQ,SAAS;AAC5B,oBAAM,UAAUF,OAAM;AACtB,oBAAM,UAAUA,OAAM;AAEtB,+BAAiB,UAAU,CAAC,YAAY;AACxC;AAAA,YACF;AAAA,UACF;AAEA,cAAI,KAAK,IAAI,WAAW,IAAI,kBAAkB,KAAK,IAAI,WAAW,IAAI,gBAAgB;AAEpF,gBAAI,eAAe,SAAS;AAC1B,2BAAa,eAAe,OAAO;AACnC,6BAAe,UAAU;AAAA,YAC3B;AACA,uBAAW,UAAU;AACrB,wBAAY,UAAU;AACtB,6BAAiB,IAAI;AACrB,YAAAD,GAAE,eAAe;AAEjB,kBAAM,UAAUC,OAAM;AACtB,kBAAM,UAAUA,OAAM;AACtB,6BAAiB,UAAU;AAAA,UAC7B;AAEA,gBAAM,UAAUA,OAAM;AACtB,gBAAM,UAAUA,OAAM;AACtB;AAAA,QACF;AAEA,QAAAD,GAAE,eAAe;AACjB,cAAM,SAASC,OAAM,UAAU,MAAM;AACrC,cAAM,SAASA,OAAM,UAAU,MAAM;AAGrC,cAAM,cAAc,SAAS,UAAU;AACvC,yBAAiB,WAAW;AAG5B,cAAM,YAAY,YAAY,UAAU,IAAI;AAC5C,YAAI,KAAK,IAAI,iBAAiB,OAAO,KAAK,WAAW;AACnD,gBAAM,aAAa,KAAK,MAAM,iBAAiB,OAAO;AACtD,2BAAiB,WAAW;AAC5B,qBAAW,QAAQ,UAAU;AAC7B,sBAAY,UAAU;AAAA,QACxB;AACA,cAAM,UAAUA,OAAM;AACtB,cAAM,UAAUA,OAAM;AAAA,MACxB;AAEA,YAAM,iBAAiB,MAAM;AAC3B,mBAAW,UAAU;AACrB,oBAAY,UAAU;AACtB,YAAI,eAAe,SAAS;AAC1B,uBAAa,eAAe,OAAO;AACnC,yBAAe,UAAU;AAAA,QAC3B;AACA,yBAAiB,KAAK;AACtB,iBAAS,oBAAoB,aAAa,eAAe;AACzD,iBAAS,oBAAoB,YAAY,cAAc;AAAA,MACzD;AAEA,eAAS,iBAAiB,aAAa,iBAAiB,EAAE,SAAS,MAAM,CAAC;AAC1E,eAAS,iBAAiB,YAAY,cAAc;AAAA,IACtD;AAAA,IACA,CAAC,UAAU,aAAa,SAAS;AAAA,EACnC;AAEA,QAAM,eAAe,cAAc,YAAY,KAAK,IAAI,OAAO,KAAK;AAEpE,QAAM,cAAc,YAAY,MAAM;AACpC,QAAI,YAAY,WAAW,QAAS;AACpC,uBAAmB,UAAU;AAC7B,iBAAa,YAAY;AACzB,iBAAa,IAAI;AAAA,EACnB,GAAG,CAAC,UAAU,YAAY,CAAC;AAE3B,QAAM,mBAAmB;AAAA,IACvB,CAAC,MAA2B;AAC1B,UAAI,SAAU;AACd,UAAI,UAAU,KAAK,EAAE,GAAG,GAAG;AACzB,UAAE,eAAe;AACjB,2BAAmB,UAAU;AAC7B,qBAAa,EAAE,GAAG;AAClB,qBAAa,IAAI;AAAA,MACnB;AAAA,IACF;AAAA,IACA,CAAC,QAAQ;AAAA,EACX;AAEA,QAAM,aAAa,YAAY,MAAM;AACnC,UAAM,SAAS,SAAS,WAAW,EAAE;AACrC,QAAI,CAAC,MAAM,MAAM,GAAG;AAClB,YAAM,UAAU,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,MAAM,CAAC;AACnD,YAAM,OAAO;AAAA,IACf;AACA,iBAAa,KAAK;AAAA,EACpB,GAAG,CAAC,WAAW,KAAK,KAAK,KAAK,CAAC;AAE/B,QAAM,gBAAgB;AAAA,IACpB,CAAC,MAA2B;AAC1B,UAAI,EAAE,QAAQ,SAAS;AACrB,mBAAW;AAAA,MACb,WAAW,EAAE,QAAQ,UAAU;AAC7B,qBAAa,KAAK;AAAA,MACpB;AAAA,IACF;AAAA,IACA,CAAC,UAAU;AAAA,EACb;AAEA,QAAM,mBAAmB,YAAY,CAAC,MAA0C;AAC9E,QAAI,CAAC,mBAAmB,SAAS;AAC/B,QAAE,OAAO,OAAO;AAAA,IAClB;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,MAAI,WAAW;AACb,WACE;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,WAAU;AAAA,QACV,OAAO;AAAA,QACP,UAAU,CAAC,MAAM,aAAa,EAAE,OAAO,MAAM,QAAQ,OAAO,EAAE,EAAE,MAAM,EAAE,CAAC;AAAA,QACzE,SAAS;AAAA,QACT,QAAQ;AAAA,QACR,WAAW;AAAA,QACX,WAAW,4CAA4C,aAAa,EAAE;AAAA,QACtE,WAAS;AAAA;AAAA,IACX;AAAA,EAEJ;AAEA,SACE,qBAAC,SAAI,WAAU,2BACZ;AAAA,qBACC,oBAAC,SAAI,WAAU,uBAAsB,eAAY,QAC9C,wBACH;AAAA,IAEF;AAAA,MAAC;AAAA;AAAA,QACC,aAAa;AAAA,QACb,cAAc;AAAA,QACd,SAAS;AAAA,QACT,WAAW;AAAA,QACX,WAAW,mBAAmB,aAAa,EAAE,IAAI,WAAW,8BAA8B,EAAE;AAAA,QAC5F,MAAK;AAAA,QACL,iBAAe;AAAA,QACf,iBAAe;AAAA,QACf,UAAU,WAAW,KAAK;AAAA,QAEzB;AAAA;AAAA,IACH;AAAA,KACF;AAEJ;AAEA,SAAS,UAAU,MAAkD;AACnE,QAAM,CAAC,GAAG,CAAC,IAAI,KAAK,MAAM,GAAG,EAAE,IAAI,MAAM;AACzC,SAAO,EAAE,OAAO,KAAK,GAAG,SAAS,KAAK,EAAE;AAC1C;AAEA,SAAS,WAAW,OAAe,SAAyB;AAC1D,SAAO,GAAG,KAAK,IAAI,OAAO,OAAO,EAAE,SAAS,GAAG,GAAG,CAAC;AACrD;AAEA,SAAS,kBAAkB,WAA2B;AACpD,SAAO,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,KAAK,IAAI,SAAS,CAAC;AACtD;AAEO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,aAAa;AAAA,EACb,kBAAkB;AAAA,EAClB,YAAY;AACd,GAAoB;AAClB,QAAM,aAAa,YAAY;AAC/B,QAAM,EAAE,OAAO,QAAQ,IAAI,UAAU,KAAK;AAC1C,QAAM,eAAe,QAAQ,KAAK;AAElC,QAAM,qBAAqB,CAAC,MAA2C;AACrE,QAAI,SAAU;AACd,UAAM,OAAO,kBAAkB,SAAS,EAAE,OAAO,KAAK,CAAC;AACvD,UAAM,IAAI,KAAK,MAAM,OAAO,EAAE;AAC9B,UAAM,IAAI,OAAO;AACjB,aAAS,WAAW,GAAG,CAAC,CAAC;AAAA,EAC3B;AAEA,QAAM,mBAAmB;AAAA,IACvB,CAAC,UAAkB;AACjB,YAAM,WAAW,KAAK,IAAI,GAAG,KAAK,IAAI,IAAI,QAAQ,KAAK,CAAC;AACxD,eAAS,WAAW,UAAU,OAAO,CAAC;AAAA,IACxC;AAAA,IACA,CAAC,OAAO,SAAS,QAAQ;AAAA,EAC3B;AAEA,QAAM,iBAAiB;AAAA,IACrB,CAAC,aAAqB;AACpB,eAAS,WAAW,UAAU,OAAO,CAAC;AAAA,IACxC;AAAA,IACA,CAAC,SAAS,QAAQ;AAAA,EACpB;AAEA,QAAM,qBAAqB;AAAA,IACzB,CAAC,UAAkB;AACjB,YAAM,kBAAkB,kBAAkB,eAAe,KAAK;AAC9D,YAAM,IAAI,KAAK,MAAM,kBAAkB,EAAE;AACzC,YAAM,IAAI,kBAAkB;AAC5B,eAAS,WAAW,GAAG,CAAC,CAAC;AAAA,IAC3B;AAAA,IACA,CAAC,cAAc,QAAQ;AAAA,EACzB;AAEA,QAAM,mBAAmB;AAAA,IACvB,CAAC,eAAuB;AACtB,eAAS,WAAW,OAAO,UAAU,CAAC;AAAA,IACxC;AAAA,IACA,CAAC,OAAO,QAAQ;AAAA,EAClB;AAEA,SACE,qBAAC,SAAI,WAAW,aAAa,aAAa,EAAE,IAAI,WAAW,wBAAwB,EAAE,IAClF;AAAA,aAAS,oBAAC,WAAM,WAAU,mBAAmB,iBAAM;AAAA,IAEpD,qBAAC,SAAI,WAAU,uBACb;AAAA,2BAAC,SAAI,WAAU,qBACb;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,OAAO;AAAA,YACP,SAAS;AAAA,YACT,OAAO;AAAA,YACP;AAAA,YACA,aAAa,kBAAkB;AAAA,YAC/B,KAAK;AAAA,YACL,KAAK;AAAA,YACL,WAAU;AAAA;AAAA,QACZ;AAAA,QACA,oBAAC,UAAK,WAAU,uBAAsB,eAAC;AAAA,QACvC;AAAA,UAAC;AAAA;AAAA,YACC,OAAO;AAAA,YACP,SAAS;AAAA,YACT,OAAO;AAAA,YACP,aAAa,CAAC,MAAM,OAAO,CAAC,EAAE,SAAS,GAAG,GAAG;AAAA,YAC7C;AAAA,YACA,aAAa;AAAA,YACb,KAAK;AAAA,YACL,KAAK;AAAA,YACL,WAAU;AAAA;AAAA,QACZ;AAAA,SACF;AAAA,MAEA,qBAAC,SAAI,WAAU,8BACb;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,KAAK;AAAA,YACL,KAAK,KAAK,KAAK;AAAA,YACf,MAAM;AAAA,YACN,OAAO;AAAA,YACP,UAAU;AAAA,YACV;AAAA,YACA,UAAU;AAAA,YACV,WAAU;AAAA,YACV,cAAW;AAAA;AAAA,QACb;AAAA,QACA,oBAAC,SAAI,WAAU,2BACZ,gBAAM,KAAK,EAAE,QAAQ,WAAW,GAAG,CAAC,GAAG,MAAM;AAC5C,gBAAM,OAAO,KAAK,MAAO,KAAK,YAAa,CAAC;AAC5C,gBAAM,UAAW,IAAI,YAAa;AAClC,iBACE,oBAAC,UAAa,OAAO,EAAE,MAAM,GAAG,OAAO,IAAI,GACxC,kBADQ,CAEX;AAAA,QAEJ,CAAC,GACH;AAAA,SACF;AAAA,OACF;AAAA,KACF;AAEJ;","names":["e","touch","deltaX","deltaY"]}
package/dist/styles.css CHANGED
@@ -63,6 +63,39 @@
63
63
  gap: 0.25rem;
64
64
  }
65
65
 
66
+ .scrubtime-value-wrapper {
67
+ position: relative;
68
+ }
69
+
70
+ .scrubtime-magnifier {
71
+ position: absolute;
72
+ bottom: 100%;
73
+ left: 50%;
74
+ transform: translateX(-50%);
75
+ margin-bottom: 0.25rem;
76
+ background: var(--scrubtime-bg);
77
+ border: 2px solid var(--scrubtime-slider-thumb);
78
+ border-radius: var(--scrubtime-radius-sm);
79
+ padding: 0.4rem 0.6rem;
80
+ font-size: 1.1rem;
81
+ font-family: var(--scrubtime-font-mono);
82
+ color: var(--scrubtime-text);
83
+ white-space: nowrap;
84
+ pointer-events: none;
85
+ z-index: 1000;
86
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
87
+ }
88
+
89
+ .scrubtime-magnifier::after {
90
+ content: '';
91
+ position: absolute;
92
+ top: 100%;
93
+ left: 50%;
94
+ transform: translateX(-50%);
95
+ border: 5px solid transparent;
96
+ border-top-color: var(--scrubtime-slider-thumb);
97
+ }
98
+
66
99
  .scrubtime-value {
67
100
  width: 3.5rem;
68
101
  background: var(--scrubtime-value-bg);
@@ -116,6 +149,9 @@
116
149
  display: flex;
117
150
  flex-direction: column;
118
151
  --thumb-width: 1.25rem;
152
+ padding: 0.75rem 0;
153
+ margin: -0.75rem 0;
154
+ user-select: none;
119
155
  }
120
156
 
121
157
  .scrubtime-slider {
@@ -128,6 +164,7 @@
128
164
  cursor: pointer;
129
165
  margin: 0;
130
166
  touch-action: pan-x;
167
+ user-select: none;
131
168
  }
132
169
 
133
170
  .scrubtime-slider::-webkit-slider-thumb {
@@ -182,6 +219,8 @@
182
219
  /* Inset to match thumb travel range */
183
220
  margin-left: calc(var(--thumb-width) / 2);
184
221
  margin-right: calc(var(--thumb-width) / 2 + 1px);
222
+ user-select: none;
223
+ pointer-events: none;
185
224
  }
186
225
 
187
226
  .scrubtime-slider-labels span {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scrubtime",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "A React time picker with draggable scrubber and slider - minimal clicks, maximum control",
5
5
  "author": "falkenhawk",
6
6
  "license": "MIT",