scrubtime 0.3.0 → 0.3.2

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
@@ -42,6 +42,7 @@ function DraggableValue({
42
42
  const [isEditing, setIsEditing] = (0, import_react.useState)(false);
43
43
  const [editValue, setEditValue] = (0, import_react.useState)("");
44
44
  const [showMagnifier, setShowMagnifier] = (0, import_react.useState)(false);
45
+ const enteredViaKeyboard = (0, import_react.useRef)(false);
45
46
  const isDragging = (0, import_react.useRef)(false);
46
47
  const hasDragged = (0, import_react.useRef)(false);
47
48
  const isLongPress = (0, import_react.useRef)(false);
@@ -179,6 +180,7 @@ function DraggableValue({
179
180
  const displayValue = formatValue ? formatValue(value) : String(value);
180
181
  const handleClick = (0, import_react.useCallback)(() => {
181
182
  if (disabled || hasDragged.current) return;
183
+ enteredViaKeyboard.current = false;
182
184
  setEditValue(displayValue);
183
185
  setIsEditing(true);
184
186
  }, [disabled, displayValue]);
@@ -187,6 +189,7 @@ function DraggableValue({
187
189
  if (disabled) return;
188
190
  if (/^[0-9]$/.test(e.key)) {
189
191
  e.preventDefault();
192
+ enteredViaKeyboard.current = true;
190
193
  setEditValue(e.key);
191
194
  setIsEditing(true);
192
195
  }
@@ -211,6 +214,11 @@ function DraggableValue({
211
214
  },
212
215
  [commitEdit]
213
216
  );
217
+ const handleInputFocus = (0, import_react.useCallback)((e) => {
218
+ if (!enteredViaKeyboard.current) {
219
+ e.target.select();
220
+ }
221
+ }, []);
214
222
  if (isEditing) {
215
223
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
216
224
  "input",
@@ -219,7 +227,7 @@ function DraggableValue({
219
227
  inputMode: "numeric",
220
228
  value: editValue,
221
229
  onChange: (e) => setEditValue(e.target.value.replace(/\D/g, "").slice(-2)),
222
- onFocus: (e) => e.target.select(),
230
+ onFocus: handleInputFocus,
223
231
  onBlur: commitEdit,
224
232
  onKeyDown: handleKeyDown,
225
233
  className: `scrubtime-value scrubtime-value--editing ${className || ""}`,
@@ -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 [showMagnifier, setShowMagnifier] = useState(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 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 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":"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","names":["e","touch","deltaX","deltaY"]}
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":"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","names":["e","touch","deltaX","deltaY"]}
package/dist/index.js CHANGED
@@ -16,6 +16,7 @@ function DraggableValue({
16
16
  const [isEditing, setIsEditing] = useState(false);
17
17
  const [editValue, setEditValue] = useState("");
18
18
  const [showMagnifier, setShowMagnifier] = useState(false);
19
+ const enteredViaKeyboard = useRef(false);
19
20
  const isDragging = useRef(false);
20
21
  const hasDragged = useRef(false);
21
22
  const isLongPress = useRef(false);
@@ -153,6 +154,7 @@ function DraggableValue({
153
154
  const displayValue = formatValue ? formatValue(value) : String(value);
154
155
  const handleClick = useCallback(() => {
155
156
  if (disabled || hasDragged.current) return;
157
+ enteredViaKeyboard.current = false;
156
158
  setEditValue(displayValue);
157
159
  setIsEditing(true);
158
160
  }, [disabled, displayValue]);
@@ -161,6 +163,7 @@ function DraggableValue({
161
163
  if (disabled) return;
162
164
  if (/^[0-9]$/.test(e.key)) {
163
165
  e.preventDefault();
166
+ enteredViaKeyboard.current = true;
164
167
  setEditValue(e.key);
165
168
  setIsEditing(true);
166
169
  }
@@ -185,6 +188,11 @@ function DraggableValue({
185
188
  },
186
189
  [commitEdit]
187
190
  );
191
+ const handleInputFocus = useCallback((e) => {
192
+ if (!enteredViaKeyboard.current) {
193
+ e.target.select();
194
+ }
195
+ }, []);
188
196
  if (isEditing) {
189
197
  return /* @__PURE__ */ jsx(
190
198
  "input",
@@ -193,7 +201,7 @@ function DraggableValue({
193
201
  inputMode: "numeric",
194
202
  value: editValue,
195
203
  onChange: (e) => setEditValue(e.target.value.replace(/\D/g, "").slice(-2)),
196
- onFocus: (e) => e.target.select(),
204
+ onFocus: handleInputFocus,
197
205
  onBlur: commitEdit,
198
206
  onKeyDown: handleKeyDown,
199
207
  className: `scrubtime-value scrubtime-value--editing ${className || ""}`,
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 [showMagnifier, setShowMagnifier] = useState(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 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 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":"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","names":["e","touch","deltaX","deltaY"]}
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":"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","names":["e","touch","deltaX","deltaY"]}
package/dist/styles.css CHANGED
@@ -54,9 +54,13 @@
54
54
  display: flex;
55
55
  flex-direction: column;
56
56
  gap: 0.5rem;
57
+ user-select: none;
58
+ cursor: default;
57
59
  }
58
60
 
59
61
  .scrubtime-display {
62
+ position: relative;
63
+ z-index: 1;
60
64
  display: flex;
61
65
  align-items: center;
62
66
  justify-content: center;
@@ -135,6 +139,7 @@
135
139
  border-radius: var(--scrubtime-radius-sm);
136
140
  padding: 0.5rem;
137
141
  outline: 2px solid var(--scrubtime-slider-thumb);
142
+ cursor: text;
138
143
  outline-offset: 0;
139
144
  box-sizing: border-box;
140
145
  }
@@ -146,6 +151,7 @@
146
151
  }
147
152
 
148
153
  .scrubtime-slider-container {
154
+ position: relative;
149
155
  display: flex;
150
156
  flex-direction: column;
151
157
  --thumb-width: 1.25rem;
@@ -154,17 +160,29 @@
154
160
  user-select: none;
155
161
  }
156
162
 
163
+ .scrubtime-slider-container::before {
164
+ content: '';
165
+ position: absolute;
166
+ top: 0.75rem;
167
+ left: 0;
168
+ right: 0;
169
+ height: 0.5rem;
170
+ background: var(--scrubtime-slider-bg);
171
+ border-radius: 0.25rem;
172
+ pointer-events: none;
173
+ }
174
+
157
175
  .scrubtime-slider {
158
176
  -webkit-appearance: none;
159
177
  appearance: none;
178
+ position: relative;
160
179
  width: 100%;
161
- height: 0.5rem;
162
- background: var(--scrubtime-slider-bg);
163
- border-radius: 0.25rem;
180
+ height: 2.1875rem;
181
+ margin: -0.875rem 0 -1.4375rem 0;
164
182
  cursor: pointer;
165
- margin: 0;
166
183
  touch-action: pan-x;
167
184
  user-select: none;
185
+ background: transparent;
168
186
  }
169
187
 
170
188
  .scrubtime-slider::-webkit-slider-thumb {
@@ -215,7 +233,7 @@
215
233
  height: 1em;
216
234
  font-size: 0.625rem;
217
235
  color: var(--scrubtime-text-muted);
218
- margin-top: 0.25rem;
236
+ margin-top: 0.875rem;
219
237
  /* Inset to match thumb travel range */
220
238
  margin-left: calc(var(--thumb-width) / 2);
221
239
  margin-right: calc(var(--thumb-width) / 2 + 1px);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scrubtime",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "A React time picker with draggable scrubber and slider - minimal clicks, maximum control",
5
5
  "author": "falkenhawk",
6
6
  "license": "MIT",