shadimage
kinetic
uiscrub-number-fieldsource: ok · refreshed

Scrub Number Field

A Figma-like scrub number input with drag-to-scrub, click-to-edit, digit animation by Calligraph, Up/Down arrow nudging, and wheel nudging.

npx shadcn add "https://kinetic.itsjay.in/r/scrub-number-field.json"
free

Interaction

drag

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · medium
iconslucide-react
animationmotion

Registry dependencies

inputinput-group

Files5 · source cached 10/7/2026, 7:01:00 PM

components/ui/scrub-number-input.tsx
"use client"

import {
  useCallback,
  useEffect,
  useLayoutEffect,
  useMemo,
  useRef,
  useState,
  type ChangeEvent,
  type ComponentProps,
  type CSSProperties,
  type KeyboardEvent,
  type PointerEvent,
  type RefObject,
} from "react"
import { Calligraph } from "calligraph"
import {
  GripHorizontal,
  GripVertical,
  Move,
  MoveHorizontal,
  MoveVertical,
  Percent,
  type LucideIcon,
} from "lucide-react"
import { useReducedMotion, motion } from "motion/react"

import { InputGroup, InputGroupAddon } from "@/components/ui/input-group"
import { Input } from "@/components/ui/input"
import { useControllableState } from "@/hooks/use-controllable-state"
import { useDisplayOverflowTruncated } from "@/lib/scrub-number-overflow"
import {
  boundOverflow,
  clampNumber,
  countDraftDecimalPlaces,
  formatDisplayValue,
  getAtBound,
  getBoundEdge,
  getScrubPointerDelta,
  hasExceededScrubThreshold,
  isFineModifierPressed,
  applyStepDelta,
  consumeWheelDelta,
  normalizeCoarseModifier,
  normalizeFineModifier,
  normalizeFiniteNumber,
  normalizeNumberFieldBounds,
  normalizePositiveFiniteStep,
  normalizeScrubThreshold,
  normalizeWheelDelta,
  normalizeWheelSensitivity,
  quantizeNumber,
  resolveActiveStep,
  resolveFineStep,
  resolveQuantizeStep,
  resolveExclusiveModifiers,
  resolveScrubStepModifiers,
  preserveDisplayDraft,
  resolveDisplayDecimalPlaces,
  sanitizeNumericDraft,
  toModifierKeys,
  type CoarseModifier,
  type FineModifier,
} from "@/lib/scrub-number-math"
import { cn } from "@/lib/utils"
import { cva } from "class-variance-authority"

import "./scrub-number-input.css"

export {
  boundOverflow,
  clampNumber,
  countDraftDecimalPlaces,
  formatDisplayValue,
  formatMinimalDisplayValue,
  getAtBound,
  getBoundEdge,
  getDecimalPlaces,
  getCoarseModifierLabel,
  getFineModifierLabel,
  getScrubPointerDelta,
  hasExceededScrubThreshold,
  isCoarseModifierPressed,
  isFineModifierPressed,
  isModifierKeyPressed,
  applyStepDelta,
  consumeWheelDelta,
  normalizeCoarseModifier,
  normalizeFineModifier,
  normalizeFiniteNumber,
  normalizeNumberFieldBounds,
  normalizePositiveFiniteStep,
  normalizeScrubThreshold,
  normalizeWheelDelta,
  normalizeWheelSensitivity,
  quantizeNumber,
  resolveActiveStep,
  resolveCoarseModifierKey,
  resolveFineModifierKey,
  resolveFineStep,
  resolveQuantizeStep,
  resolveExclusiveModifiers,
  resolveScrubStepModifiers,
  preserveDisplayDraft,
  resolveDisplayDecimalPlaces,
  toModifierKeys,
  getValueDecimalPlaces,
  stepFromDecimalPlaces,
  MODIFIER_OPTIONS,
  type CoarseModifier,
  type FineModifier,
  type DisplayFormat,
  type ModifierKey,
} from "@/lib/scrub-number-math"

const SCRUB_NUMBER_FIELD_CLASS = "tabular-nums"

const SCRUB_NUMBER_SPINNER_HIDE_CLASS =
  "[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"

const scrubFieldVariants = cva(
  "w-full min-w-0 rounded-[12px] border border-input bg-[var(--input-fill)] py-1 text-start text-base text-foreground transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 h-7 px-2 text-[0.8rem]",
)

export type InputSettings = {
  selectOnEdit: boolean
}

export const DEFAULT_INPUT_SETTINGS: InputSettings = {
  selectOnEdit: true,
}

export type FormatSettings = {
  alwaysShowSign: boolean
}

export const DEFAULT_FORMAT_SETTINGS: FormatSettings = {
  alwaysShowSign: false,
}

export type BoundFeedbackMode =
  | "none"
  | "shake"
  | "borderPulse"

export const BOUND_FEEDBACK_MODES = [
  "none",
  "shake",
  "borderPulse",
] as const satisfies readonly BoundFeedbackMode[]

export type BoundFeedbackSource = "wheel" | "key" | "scrub"

export type BoundFeedbackState = {
  edge: "min" | "max"
  overflow: number
  source: BoundFeedbackSource
  tick: number
}

export type ScrubSettings = {
  direction: "horizontal" | "vertical"
  shiftStep: number
  sensitivity: number
  threshold?: number
  wheelEnabled: boolean
  boundFeedback: BoundFeedbackMode
  fineStep?: number
  fineModifier?: FineModifier
  coarseModifier?: CoarseModifier
  wheelSensitivity?: number
}

export const DEFAULT_SCRUB_SETTINGS: ScrubSettings = {
  direction: "horizontal",
  shiftStep: 10,
  sensitivity: 1,
  threshold: 3,
  wheelEnabled: false,
  boundFeedback: "none",
  fineModifier: "alt",
  coarseModifier: "shift",
  wheelSensitivity: 20,
}

export type CalligraphSettings = {
  variant: "number" | "slots"
  animation: "default" | "smooth" | "snappy" | "bouncy"
  stagger: number
  autoSize: boolean
}

export const DEFAULT_CALLIGRAPH_SETTINGS: CalligraphSettings = {
  variant: "slots",
  animation: "snappy",
  stagger: 0.02,
  autoSize: false,
}

const VALUE_NUDGE_KEYS = new Set([
  "ArrowUp",
  "ArrowDown",
  "PageUp",
  "PageDown",
])

export const LOGO_ICON_OPTIONS = [
  "GripVertical",
  "GripHorizontal",
  "Move",
  "MoveHorizontal",
  "MoveVertical",
  "Percent",
] as const

export type LogoIconName = (typeof LOGO_ICON_OPTIONS)[number]

export type LogoSettings = {
  enabled: boolean
  icon: LogoIconName
}

export const DEFAULT_LOGO_SETTINGS: LogoSettings = {
  enabled: false,
  icon: "GripVertical",
}

const LOGO_ICONS: Record<LogoIconName, LucideIcon> = {
  GripVertical,
  GripHorizontal,
  Move,
  MoveHorizontal,
  MoveVertical,
  Percent,
}

export function ScrubLogoIcon({
  className,
  name,
}: {
  className?: string
  name: LogoIconName
}) {
  const Icon = LOGO_ICONS[name]
  return <Icon aria-hidden className={className} />
}

export type ScrubFieldSettings = {
  calligraph: CalligraphSettings
  input: InputSettings
  logo: LogoSettings
  min?: number
  max?: number
  step?: number
  smallStep?: number
  largeStep?: number
  direction?: "horizontal" | "vertical"
  pixelSensitivity?: number
  allowWheelScrub?: boolean
  boundFeedback?: BoundFeedbackMode
  format?: Intl.NumberFormatOptions
}

export const DEFAULT_SCRUB_FIELD_SETTINGS: ScrubFieldSettings = {
  calligraph: DEFAULT_CALLIGRAPH_SETTINGS,
  input: DEFAULT_INPUT_SETTINGS,
  logo: DEFAULT_LOGO_SETTINGS,
  step: 1,
  smallStep: 0.1,
  largeStep: 10,
  direction: "horizontal",
  pixelSensitivity: 2,
  allowWheelScrub: false,
  boundFeedback: "none",
}

function flatSettingsToScrubSettings(
  settings: Pick<
    ScrubFieldSettings,
    | "smallStep"
    | "largeStep"
    | "direction"
    | "pixelSensitivity"
    | "allowWheelScrub"
    | "boundFeedback"
    | "step"
  >,
): ScrubSettings {
  const step = normalizePositiveFiniteStep(settings.step)
  let fineStep = settings.smallStep ?? 0.1
  if (!Number.isFinite(fineStep) || fineStep <= 0 || fineStep >= step) {
    fineStep = resolveFineStep(step)
  }

  let shiftStep = settings.largeStep ?? DEFAULT_SCRUB_SETTINGS.shiftStep
  if (!Number.isFinite(shiftStep) || shiftStep < step) {
    shiftStep = Math.max(step, DEFAULT_SCRUB_SETTINGS.shiftStep)
  }

  const sensitivity =
    typeof settings.pixelSensitivity === "number" &&
    Number.isFinite(settings.pixelSensitivity) &&
    settings.pixelSensitivity > 0
      ? settings.pixelSensitivity
      : DEFAULT_SCRUB_SETTINGS.sensitivity

  return {
    ...DEFAULT_SCRUB_SETTINGS,
    direction: settings.direction === "vertical" ? "vertical" : "horizontal",
    boundFeedback: settings.boundFeedback ?? "none",
    wheelEnabled: Boolean(settings.allowWheelScrub),
    sensitivity,
    fineStep,
    shiftStep,
  }
}

export function normalizeScrubFieldSettings(
  settings: ScrubFieldSettings,
): ScrubFieldSettings {
  let min = settings.min
  let max = settings.max

  if (
    min != null &&
    max != null &&
    Number.isFinite(min) &&
    Number.isFinite(max) &&
    min > max
  ) {
    ;[min, max] = [max, min]
  }

  const step =
    typeof settings.step === "number" && Number.isFinite(settings.step) && settings.step > 0
      ? settings.step
      : 1

  let smallStep = settings.smallStep ?? 0.1
  if (!Number.isFinite(smallStep) || smallStep <= 0 || smallStep >= step) {
    smallStep = Math.min(step / 10, step) || 0.1
  }

  let largeStep = settings.largeStep ?? 10
  if (!Number.isFinite(largeStep) || largeStep < step) {
    largeStep = Math.max(step, 10)
  }

  const requestedLogoIcon = settings.logo?.icon
  const logoIcon =
    requestedLogoIcon &&
    LOGO_ICON_OPTIONS.includes(requestedLogoIcon as LogoIconName)
      ? requestedLogoIcon
      : DEFAULT_LOGO_SETTINGS.icon

  const pixelSensitivity =
    typeof settings.pixelSensitivity === "number" &&
    Number.isFinite(settings.pixelSensitivity) &&
    settings.pixelSensitivity > 0
      ? settings.pixelSensitivity
      : 2

  return {
    calligraph: { ...DEFAULT_CALLIGRAPH_SETTINGS, ...settings.calligraph },
    input: { ...DEFAULT_INPUT_SETTINGS, ...settings.input },
    logo: {
      ...DEFAULT_LOGO_SETTINGS,
      ...settings.logo,
      icon: logoIcon,
    },
    min,
    max,
    step,
    smallStep,
    largeStep,
    direction: settings.direction === "vertical" ? "vertical" : "horizontal",
    pixelSensitivity,
    allowWheelScrub: Boolean(settings.allowWheelScrub),
    boundFeedback: settings.boundFeedback ?? "none",
    format: settings.format,
  }
}

export function getScrubCursorClass(
  scrub: Pick<ScrubSettings, "direction">,
  atBound: "min" | "max" | null = null,
  bounds?: { min?: number; max?: number },
) {
  if (
    bounds?.min != null &&
    bounds?.max != null &&
    bounds.min === bounds.max
  ) {
    return "cursor-not-allowed"
  }

  if (scrub.direction === "vertical") {
    if (atBound === "min") {
      return "cursor-n-resize"
    }

    if (atBound === "max") {
      return "cursor-s-resize"
    }

    return "cursor-ns-resize"
  }

  if (atBound === "min") {
    return "cursor-e-resize"
  }

  if (atBound === "max") {
    return "cursor-w-resize"
  }

  return "cursor-ew-resize"
}

type EditPointerPoint = {
  clientX: number
  clientY: number
}

function approximateCaretFromX(input: HTMLInputElement, clientX: number) {
  const text = input.value
  const rect = input.getBoundingClientRect()
  const style = getComputedStyle(input)
  const paddingLeft = Number.parseFloat(style.paddingLeft) || 0
  const paddingRight = Number.parseFloat(style.paddingRight) || 0
  const contentWidth = rect.width - paddingLeft - paddingRight

  const canvas = document.createElement("canvas")
  const ctx = canvas.getContext("2d")

  if (!ctx) {
    input.setSelectionRange(text.length, text.length)
    scrollCaretIntoView(input)
    return
  }

  ctx.font = `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${style.fontSize} ${style.fontFamily}`
  ctx.letterSpacing = style.letterSpacing

  const textWidth = ctx.measureText(text).width
  let textStart = paddingLeft + input.scrollLeft

  const textAlign = style.textAlign

  if (textAlign === "center") {
    textStart += Math.max(0, (contentWidth - textWidth) / 2)
  } else if (textAlign === "right" || textAlign === "end") {
    textStart += Math.max(0, contentWidth - textWidth)
  }

  const relativeX = clientX - rect.left - textStart

  if (relativeX <= 0) {
    input.setSelectionRange(0, 0)
    scrollCaretIntoView(input)
    return
  }

  if (relativeX >= textWidth) {
    input.setSelectionRange(text.length, text.length)
    scrollCaretIntoView(input)
    return
  }

  let offset = 0

  for (let index = 1; index <= text.length; index++) {
    const width = ctx.measureText(text.slice(0, index)).width

    if (width >= relativeX) {
      const previousWidth =
        index > 1 ? ctx.measureText(text.slice(0, index - 1)).width : 0
      const characterWidth = ctx.measureText(text[index - 1] ?? "").width
      offset = relativeX - previousWidth < characterWidth / 2 ? index - 1 : index
      break
    }

    offset = index
  }

  input.setSelectionRange(offset, offset)
  scrollCaretIntoView(input)
}

function placeCaretAtPoint(
  input: HTMLInputElement,
  clientX: number,
  clientY: number,
) {
  const doc = input.ownerDocument

  if (typeof doc.caretRangeFromPoint === "function") {
    const range = doc.caretRangeFromPoint(clientX, clientY)

    if (range && input.contains(range.startContainer)) {
      const offset = Math.min(range.startOffset, input.value.length)
      input.setSelectionRange(offset, offset)
      scrollCaretIntoView(input)
      return true
    }
  }

  if (typeof doc.caretPositionFromPoint === "function") {
    const position = doc.caretPositionFromPoint(clientX, clientY)

    if (
      position &&
      (input === position.offsetNode || input.contains(position.offsetNode))
    ) {
      const offset = Math.min(position.offset, input.value.length)
      input.setSelectionRange(offset, offset)
      scrollCaretIntoView(input)
      return true
    }
  }

  approximateCaretFromX(input, clientX)
  return true
}

function inputTextOverflows(input: HTMLInputElement) {
  return input.scrollWidth > input.clientWidth + 1
}

function scrollCaretIntoView(input: HTMLInputElement) {
  if (!inputTextOverflows(input)) {
    return
  }

  const caret = input.selectionStart ?? input.value.length
  const style = getComputedStyle(input)
  const paddingLeft = Number.parseFloat(style.paddingLeft) || 0
  const paddingRight = Number.parseFloat(style.paddingRight) || 0
  const contentWidth = input.clientWidth - paddingLeft - paddingRight
  const canvas = document.createElement("canvas")
  const ctx = canvas.getContext("2d")

  if (!ctx || contentWidth <= 0) {
    return
  }

  ctx.font = `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${style.fontSize} ${style.fontFamily}`
  ctx.letterSpacing = style.letterSpacing

  const textBeforeCaret = input.value.slice(0, caret)
  const caretX = ctx.measureText(textBeforeCaret).width
  const maxScroll = Math.max(0, input.scrollWidth - input.clientWidth)

  if (caretX - input.scrollLeft < paddingLeft) {
    input.scrollLeft = Math.max(0, caretX - paddingLeft)
    return
  }

  const visibleEnd = input.scrollLeft + contentWidth - paddingRight

  if (caretX > visibleEnd) {
    input.scrollLeft = Math.min(maxScroll, caretX - contentWidth + paddingRight)
  }
}

function focusCaretAtEnd(input: HTMLInputElement) {
  const length = input.value.length
  input.setSelectionRange(length, length)
  scrollCaretIntoView(input)
}

function focusInputForEdit(
  input: HTMLInputElement,
  selectOnEdit: boolean,
  point?: EditPointerPoint,
) {
  input.focus({ preventScroll: true })

  if (selectOnEdit) {
    input.select()
    scrollCaretIntoView(input)
    return
  }

  if (point) {
    placeCaretAtPoint(input, point.clientX, point.clientY)
    return
  }

  focusCaretAtEnd(input)
}

const SCRUB_BOUND_FEEDBACK_MS = 80 * 2 + 60 * 2

const SCRUB_BOUND_REVERT_HOLD_MS = 600

function restartBoundShake(targets: HTMLElement[]) {
  for (const element of targets) {
    element.classList.remove("is-shaking")
    void element.offsetWidth
    element.classList.add("is-shaking")
  }
}

function clearBoundShake(targets: HTMLElement[]) {
  for (const element of targets) {
    element.classList.remove("is-shaking")
  }
}

function ScrubBoundFeedback({
  boundFeedback,
  children,
  className,
  mode,
  onFeedbackComplete,
}: {
  boundFeedback: BoundFeedbackState | null
  children: React.ReactNode
  className?: string
  mode: BoundFeedbackMode
  onFeedbackComplete: () => void
}) {
  const shouldReduceMotion = useReducedMotion()
  const wrapRef = useRef<HTMLDivElement>(null)
  const [boundHit, setBoundHit] = useState<"min" | "max" | null>(null)
  const [boundError, setBoundError] = useState(false)

  useEffect(() => {
    if (!boundFeedback || mode === "none") {
      setBoundError(false)
      return
    }

    const shakeTargets = wrapRef.current
      ? Array.from(
          wrapRef.current.querySelectorAll<HTMLElement>(".scrub-bound-field"),
        )
      : []

    if (mode === "shake") {
      setBoundError(true)

      if (!shouldReduceMotion && shakeTargets.length > 0) {
        restartBoundShake(shakeTargets)
      }

      const completeTimer = window.setTimeout(() => {
        onFeedbackComplete()
      }, shouldReduceMotion ? 0 : SCRUB_BOUND_FEEDBACK_MS)

      const revertTimer = window.setTimeout(() => {
        setBoundError(false)
        clearBoundShake(shakeTargets)
      }, shouldReduceMotion
        ? 0
        : SCRUB_BOUND_FEEDBACK_MS + SCRUB_BOUND_REVERT_HOLD_MS)

      return () => {
        window.clearTimeout(completeTimer)
        window.clearTimeout(revertTimer)
      }
    }

    if (mode === "borderPulse") {
      setBoundHit(boundFeedback.edge)
      const timeout = window.setTimeout(() => {
        setBoundHit(null)
        onFeedbackComplete()
      }, shouldReduceMotion ? 0 : SCRUB_BOUND_FEEDBACK_MS)

      return () => {
        window.clearTimeout(timeout)
      }
    }
  }, [
    boundFeedback,
    mode,
    onFeedbackComplete,
    shouldReduceMotion,
  ])

  return (
    <div className={className}>
      <div
        ref={wrapRef}
        data-slot="scrub-bound-feedback"
        data-bound-hit={boundHit ?? undefined}
        className={cn(
          "scrub-bound-wrap",
          boundError && "is-bound-error",
        )}
      >
        {children}
      </div>
    </div>
  )
}

export type UseNumberScrubOptions = {
  disabled?: boolean
  format?: FormatSettings
  formatValue?: (value: number) => string
  logo?: LogoSettings
  max?: number
  min?: number
  onChange: (value: number) => void
  onValueCommit?: (value: number) => void
  defaultResetValue?: number
  scrub?: ScrubSettings
  selectOnEdit?: boolean
  shiftStep?: number
  step?: number
  value: number
}

export type ScrubState = ReturnType<typeof useNumberScrub>

export function useNumberScrub({
  disabled = false,
  format = DEFAULT_FORMAT_SETTINGS,
  formatValue,
  logo = DEFAULT_LOGO_SETTINGS,
  max: maxProp,
  min: minProp,
  onChange,
  onValueCommit,
  defaultResetValue,
  scrub = DEFAULT_SCRUB_SETTINGS,
  selectOnEdit = true,
  shiftStep: shiftStepProp,
  step: stepProp = 1,
  value: valueProp,
}: UseNumberScrubOptions) {
  const { min, max } = normalizeNumberFieldBounds(minProp, maxProp)
  const value = normalizeFiniteNumber(valueProp) ?? 0
  const step = normalizePositiveFiniteStep(stepProp)
  const effectiveShiftStep = normalizePositiveFiniteStep(
    shiftStepProp ?? scrub.shiftStep,
    Math.max(step, DEFAULT_SCRUB_SETTINGS.shiftStep),
  )
  const fineStep = resolveFineStep(step, scrub.fineStep)
  const wheelSensitivity = normalizeWheelSensitivity(scrub.wheelSensitivity)
  const fineModifier = normalizeFineModifier(
    scrub.fineModifier,
    DEFAULT_SCRUB_SETTINGS.fineModifier ?? "alt",
  )
  const coarseModifier = normalizeCoarseModifier(
    scrub.coarseModifier,
    DEFAULT_SCRUB_SETTINGS.coarseModifier ?? "shift",
  )
  const scrubThreshold = normalizeScrubThreshold(scrub.threshold)
  const logoScrollEnabled = logo.enabled
  const userDecimalPlacesRef = useRef<number | null>(null)
  const displayDecimalPlacesRef = useRef<number | null>(null)
  const wheelDeltaRef = useRef(0)
  const wheelModifierRef = useRef<string | null>(null)
  const formatForEdit = useCallback(
    (nextValue: number) =>
      formatDisplayValue(
        nextValue,
        format,
        userDecimalPlacesRef.current ?? displayDecimalPlacesRef.current,
      ),
    [format],
  )
  const formatForDisplay = useCallback(
    (nextValue: number) => {
      if (formatValue) {
        return formatValue(nextValue)
      }

      return formatForEdit(nextValue)
    },
    [formatForEdit, formatValue],
  )
  const [draft, setDraft] = useState(() => formatForEdit(value))
  const draftRef = useRef(draft)
  draftRef.current = draft
  const [editing, setEditing] = useState(false)
  const editingRef = useRef(false)
  editingRef.current = editing
  const [invalid, setInvalid] = useState(false)
  const [boundFeedback, setBoundFeedback] = useState<BoundFeedbackState | null>(
    null,
  )
  const boundFeedbackRef = useRef<BoundFeedbackState | null>(boundFeedback)
  boundFeedbackRef.current = boundFeedback
  const boundFeedbackTickRef = useRef(0)
  const boundFeedbackLatchedRef = useRef({ max: false, min: false })
  const interactingRef = useRef(false)
  const lastCommittedValueRef = useRef(value)
  const lastNudgeDirectionRef = useRef<1 | -1 | 0>(0)
  const [interactionEpoch, setInteractionEpoch] = useState(0)
  const lastClickRef = useRef<{
    time: number
    x: number
    y: number
  } | null>(null)
  const inputRef = useRef<HTMLInputElement>(null)
  const displaySurfaceRef = useRef<HTMLDivElement>(null)
  const surfaceRef = useRef<HTMLDivElement>(null)
  const scrubRef = useRef<{
    captureTarget: HTMLElement | null
    pointerId: number
    scrubbing: boolean
    source: "input" | "label"
    startValue: number
    startX: number
    startY: number
  } | null>(null)
  const scrubSessionGuardRef = useRef<(() => void) | null>(null)
  const pendingEditKeyboardNudgeRef = useRef<{
    direction: 1 | -1
    modifiers: { coarse?: boolean; fine?: boolean }
    source: BoundFeedbackSource
  } | null>(null)

  const detachScrubSessionGuard = useCallback(() => {
    scrubSessionGuardRef.current?.()
    scrubSessionGuardRef.current = null
  }, [])

  const notifyCommit = useCallback(
    (committedValue: number) => {
      onValueCommit?.(committedValue)
    },
    [onValueCommit],
  )

  const resetToDefault = useCallback(() => {
    if (defaultResetValue == null) {
      return false
    }

    const bounded = clampNumber(defaultResetValue, min, max)
    onChange(bounded)
    lastCommittedValueRef.current = bounded
    setDraft(formatForEdit(bounded))
    notifyCommit(bounded)
    return true
  }, [defaultResetValue, formatForEdit, max, min, notifyCommit, onChange])

  const finishInteraction = useCallback(() => {
    if (!interactingRef.current) {
      return
    }

    interactingRef.current = false
    setInteractionEpoch((epoch) => epoch + 1)
  }, [])

  useEffect(() => {
    if (interactingRef.current) {
      return
    }

    const parsedDraft = Number(draftRef.current.replace(/^\+/, ""))
    const isExternalChange =
      Number.isFinite(parsedDraft) &&
      parsedDraft !== value &&
      lastCommittedValueRef.current !== value

    if (isExternalChange) {
      userDecimalPlacesRef.current = null
      displayDecimalPlacesRef.current = null
    }

    const nextDraft = preserveDisplayDraft(
      draftRef.current,
      value,
      formatForEdit(value),
    )
    displayDecimalPlacesRef.current = resolveDisplayDecimalPlaces(
      nextDraft,
      userDecimalPlacesRef.current,
    )
    draftRef.current = nextDraft
    setDraft(nextDraft)
    lastCommittedValueRef.current = value
  }, [formatForEdit, value])

  useEffect(() => {
    if (min != null && value > min) {
      boundFeedbackLatchedRef.current.min = false
    }

    if (max != null && value < max) {
      boundFeedbackLatchedRef.current.max = false
    }
  }, [max, min, value])

  const clearBoundFeedback = useCallback(() => {
    if (boundFeedbackRef.current === null) {
      return
    }

    setBoundFeedback(null)
  }, [])

  const triggerBoundFeedback = useCallback(
    (
      edge: "min" | "max",
      source: BoundFeedbackSource,
      attempted: number,
    ) => {
      if (scrub.boundFeedback === "none") {
        return
      }

      if (boundFeedbackLatchedRef.current[edge]) {
        return
      }

      boundFeedbackLatchedRef.current[edge] = true

      boundFeedbackTickRef.current += 1
      setBoundFeedback({
        edge,
        overflow: boundOverflow(attempted, edge, min, max),
        source,
        tick: boundFeedbackTickRef.current,
      })
    },
    [max, min, scrub.boundFeedback],
  )

  const getCurrentNumericValue = useCallback(() => {
    const current = Number(draftRef.current.replace(/^\+/, ""))
    return Number.isFinite(current) ? current : value
  }, [value])

  const resolveCommitQuantizeStep = useCallback(
    (currentValue: number, fine = false) =>
      resolveQuantizeStep({
        step,
        fineStep,
        fine,
        currentValue,
        userDecimalPlaces: userDecimalPlacesRef.current,
      }),
    [fineStep, step],
  )

  const commit = useCallback(
    (
      nextValue: number,
      source?: BoundFeedbackSource,
      quantizeStep = step,
      activeStep = step,
      direction?: 1 | -1,
    ) => {
      const current = getCurrentNumericValue()
      const attempted = quantizeNumber(nextValue, quantizeStep)
      const bounded = clampNumber(attempted, min, max)

      if (source) {
        const edge = getBoundEdge(current, attempted, min, max)

        if (edge) {
          triggerBoundFeedback(edge, source, attempted)
        } else if (source === "scrub" && boundFeedbackRef.current !== null) {
          setBoundFeedback(null)
        }
      }

      if (direction) {
        lastNudgeDirectionRef.current = direction
      } else if (bounded !== current) {
        lastNudgeDirectionRef.current = bounded > current ? 1 : -1
      }

      onChange(bounded)
      const decimalPlaces = resolveDisplayDecimalPlaces(
        draftRef.current,
        userDecimalPlacesRef.current,
        activeStep,
      )
      displayDecimalPlacesRef.current = decimalPlaces
      const nextDraft = formatForEdit(bounded)
      draftRef.current = nextDraft
      setDraft(nextDraft)
      lastCommittedValueRef.current = bounded

      return bounded
    },
    [
      formatForEdit,
      getCurrentNumericValue,
      max,
      min,
      onChange,
      step,
      triggerBoundFeedback,
    ],
  )

  const getActiveStep = useCallback(
    (modifiers: { coarse?: boolean; fine?: boolean }) =>
      resolveActiveStep({
        step,
        shiftStep: effectiveShiftStep,
        fineStep,
        coarse: modifiers.coarse,
        fine: modifiers.fine,
      }),
    [effectiveShiftStep, fineStep, step],
  )

  const resetWheelAccumulator = useCallback(() => {
    wheelDeltaRef.current = 0
    wheelModifierRef.current = null
  }, [])

  const getStepModifiers = useCallback(
    (event: {
      shiftKey: boolean
      altKey: boolean
      metaKey: boolean
      getModifierState?: (key: string) => boolean
    }) =>
      resolveScrubStepModifiers(toModifierKeys(event), {
        fineModifier,
        coarseModifier,
      }),
    [coarseModifier, fineModifier],
  )

  const getDomStepModifiers = useCallback(
    (event: {
      shiftKey: boolean
      altKey: boolean
      metaKey: boolean
      getModifierState: (key: string) => boolean
    }) => getStepModifiers(event),
    [getStepModifiers],
  )

  const applyDisplayNudge = useCallback(
    (
      direction: 1 | -1,
      modifiers: { coarse?: boolean; fine?: boolean },
      source: BoundFeedbackSource,
      count = 1,
    ) => {
      if (editingRef.current) {
        return false
      }

      const current = getCurrentNumericValue()
      const fine = modifiers.fine ?? false
      const activeStep = getActiveStep({
        coarse: modifiers.coarse ?? false,
        fine,
      })
      const attempted = applyStepDelta(current, direction * activeStep * count, {
        step,
        fineStep,
        fine,
        userDecimalPlaces: userDecimalPlacesRef.current,
      })
      const bounded = clampNumber(attempted, min, max)

      if (bounded === current) {
        const edge = getBoundEdge(current, attempted, min, max)

        if (edge) {
          triggerBoundFeedback(edge, source, attempted)
        }

        return true
      }

      interactingRef.current = true
      lastNudgeDirectionRef.current = direction
      const decimalPlaces = resolveDisplayDecimalPlaces(
        draftRef.current,
        userDecimalPlacesRef.current,
        activeStep,
      )
      displayDecimalPlacesRef.current = decimalPlaces
      const nextDraft = formatForEdit(bounded)
      draftRef.current = nextDraft
      setDraft(nextDraft)
      lastCommittedValueRef.current = bounded
      onChange(bounded)

      return true
    },
    [
      fineStep,
      formatForEdit,
      getActiveStep,
      getCurrentNumericValue,
      max,
      min,
      onChange,
      step,
      triggerBoundFeedback,
    ],
  )

  const applyWheelNudge = useCallback(
    (event: WheelEvent) => {
      const modifiers = getDomStepModifiers({
        shiftKey: event.shiftKey,
        altKey: event.altKey,
        metaKey: event.metaKey,
        getModifierState: (key) => event.getModifierState(key as "Shift"),
      })
      const modifierKey = `${modifiers.coarse}:${modifiers.fine}`

      if (wheelModifierRef.current !== modifierKey) {
        wheelDeltaRef.current = 0
        wheelModifierRef.current = modifierKey
      }

      const normalizedDelta = normalizeWheelDelta(
        event.deltaY,
        event.deltaMode,
      )
      const { accumulated, direction, steps } = consumeWheelDelta(
        wheelDeltaRef.current,
        normalizedDelta,
        wheelSensitivity,
      )
      wheelDeltaRef.current = accumulated

      if (steps === 0 || direction === 0) {
        event.preventDefault()
        return
      }

      if (applyDisplayNudge(direction, modifiers, "wheel", steps)) {
        event.preventDefault()
      }
    },
    [applyDisplayNudge, getDomStepModifiers, wheelSensitivity],
  )

  useEffect(() => {
    const node = surfaceRef.current

    if (!node || disabled || logoScrollEnabled) {
      return
    }

    const handleWheel = (event: WheelEvent) => {
      if (!scrub.wheelEnabled) {
        return
      }

      applyWheelNudge(event)
    }

    const handlePointerLeave = () => {
      resetWheelAccumulator()
    }

    node.addEventListener("wheel", handleWheel, { passive: false })
    node.addEventListener("pointerleave", handlePointerLeave)

    return () => {
      node.removeEventListener("wheel", handleWheel)
      node.removeEventListener("pointerleave", handlePointerLeave)
    }
  }, [
    applyWheelNudge,
    disabled,
    logoScrollEnabled,
    resetWheelAccumulator,
    scrub.wheelEnabled,
  ])

  const jumpToBound = useCallback(
    (target: number) => {
      commit(target, "key")
    },
    [commit],
  )

  const handleKeyboardNudge = useCallback(
    (event: KeyboardEvent<HTMLElement>) => {
      if (disabled) {
        return false
      }

      const wasEditing =
        editingRef.current && VALUE_NUDGE_KEYS.has(event.key)

      if (wasEditing) {
        editingRef.current = false
        setEditing(false)
      }

      const modifiers = getDomStepModifiers({
        shiftKey: event.shiftKey,
        altKey: event.altKey,
        metaKey: event.metaKey,
        getModifierState: (key) => event.getModifierState(key as "Shift"),
      })

      const scheduleOrApplyNudge = (
        direction: 1 | -1,
        nudgeModifiers: { coarse?: boolean; fine?: boolean },
        source: BoundFeedbackSource,
      ) => {
        event.preventDefault()

        if (wasEditing) {
          pendingEditKeyboardNudgeRef.current = {
            direction,
            modifiers: nudgeModifiers,
            source,
          }
          return true
        }

        return applyDisplayNudge(direction, nudgeModifiers, source)
      }

      switch (event.key) {
        case "ArrowUp":
          return scheduleOrApplyNudge(1, modifiers, "key")

        case "ArrowDown":
          return scheduleOrApplyNudge(-1, modifiers, "key")

        case "PageUp":
          return scheduleOrApplyNudge(1, { coarse: true }, "key")

        case "PageDown":
          return scheduleOrApplyNudge(-1, { coarse: true }, "key")

        case "Home":
          if (min != null) {
            event.preventDefault()
            jumpToBound(min)
            return true
          }
          return false

        case "End":
          if (max != null) {
            event.preventDefault()
            jumpToBound(max)
            return true
          }
          return false

        default:
          return false
      }
    },
    [applyDisplayNudge, disabled, getDomStepModifiers, jumpToBound, max, min],
  )

  useLayoutEffect(() => {
    const pending = pendingEditKeyboardNudgeRef.current

    if (editing || !pending) {
      return
    }

    pendingEditKeyboardNudgeRef.current = null
    applyDisplayNudge(pending.direction, pending.modifiers, pending.source)
  }, [applyDisplayNudge, editing])

  useEffect(() => {
    const handleKeyUp = (event: globalThis.KeyboardEvent) => {
      if (VALUE_NUDGE_KEYS.has(event.key)) {
        requestAnimationFrame(() => {
          finishInteraction()
        })
      }
    }

    window.addEventListener("keyup", handleKeyUp)

    return () => {
      window.removeEventListener("keyup", handleKeyUp)
    }
  }, [finishInteraction])

  const enterEditMode = useCallback(
    (point?: EditPointerPoint) => {
      if (disabled) {
        return
      }

      editingRef.current = true
      setEditing(true)
      interactingRef.current = true
      setDraft((current) =>
        preserveDisplayDraft(current, value, formatForEdit(value)),
      )

      requestAnimationFrame(() => {
        const input = inputRef.current

        if (!input) {
          return
        }

        requestAnimationFrame(() => {
          if (!inputRef.current) {
            return
          }

          focusInputForEdit(inputRef.current, selectOnEdit, point)
        })
      })
    },
    [disabled, formatForEdit, selectOnEdit, value],
  )

  const canScrub = !disabled && !editing

  const endScrubSession = useCallback(
    (event: PointerEvent<HTMLElement>, allowEditOnClick: boolean) => {
      const state = scrubRef.current

      if (!state) {
        return
      }

      const wasScrubbing = state.scrubbing
      scrubRef.current = null
      detachScrubSessionGuard()

      if (state.captureTarget) {
        try {
          state.captureTarget.releasePointerCapture(event.pointerId)
        } catch {
        }
      }

      if (wasScrubbing) {
        finishInteraction()

        if (boundFeedbackRef.current !== null) {
          setBoundFeedback(null)
        }

        setDraft((current) => {
          const nextDraft = preserveDisplayDraft(
            current,
            value,
            formatForEdit(value),
          )
          draftRef.current = nextDraft
          displayDecimalPlacesRef.current = resolveDisplayDecimalPlaces(
            nextDraft,
            userDecimalPlacesRef.current,
          )
          return nextDraft
        })
        notifyCommit(value)
        event.preventDefault()
        return
      }

      if (
        isFineModifierPressed(
          toModifierKeys({
            shiftKey: event.shiftKey,
            altKey: event.altKey,
            metaKey: event.metaKey,
            getModifierState: (key) => event.getModifierState(key as "Shift"),
          }),
          fineModifier,
        ) &&
        resetToDefault()
      ) {
        event.preventDefault()
        return
      }

      const now = Date.now()
      const lastClick = lastClickRef.current

      if (
        lastClick &&
        now - lastClick.time < 300 &&
        Math.hypot(event.clientX - lastClick.x, event.clientY - lastClick.y) < 5
      ) {
        lastClickRef.current = null

        if (state.source === "label" && resetToDefault()) {
          event.preventDefault()
          return
        }
      } else {
        lastClickRef.current = {
          time: now,
          x: event.clientX,
          y: event.clientY,
        }
      }

      if (allowEditOnClick && state.source === "input") {
        enterEditMode({
          clientX: event.clientX,
          clientY: event.clientY,
        })
      }
    },
    [detachScrubSessionGuard, enterEditMode, fineModifier, finishInteraction, formatForEdit, notifyCommit, resetToDefault, value],
  )

  const attachScrubSessionGuard = useCallback(() => {
    detachScrubSessionGuard()

    const handleGlobalPointerEnd = (event: globalThis.PointerEvent) => {
      const state = scrubRef.current

      if (!state || event.pointerId !== state.pointerId) {
        return
      }

      endScrubSession(
        event as unknown as PointerEvent<HTMLElement>,
        state.source === "input",
      )
    }

    document.addEventListener("pointerup", handleGlobalPointerEnd)
    document.addEventListener("pointercancel", handleGlobalPointerEnd)

    scrubSessionGuardRef.current = () => {
      document.removeEventListener("pointerup", handleGlobalPointerEnd)
      document.removeEventListener("pointercancel", handleGlobalPointerEnd)
    }
  }, [detachScrubSessionGuard, endScrubSession])

  const beginPointerCapture = useCallback(
    (captureTarget: HTMLElement | null, pointerId: number) => {
      const state = scrubRef.current

      if (!state || !captureTarget) {
        return
      }

      state.captureTarget = captureTarget
      captureTarget.blur()

      try {
        captureTarget.setPointerCapture(pointerId)
      } catch {
      }
    },
    [],
  )

  const activateScrubbing = useCallback(
    (
      event: PointerEvent<HTMLElement>,
      captureTarget: HTMLElement | null,
    ) => {
      const state = scrubRef.current

      if (!state || state.scrubbing) {
        return
      }

      state.scrubbing = true
      interactingRef.current = true
      event.preventDefault()
      beginPointerCapture(captureTarget, event.pointerId)
    },
    [beginPointerCapture],
  )

  const applyScrubDelta = useCallback(
    (event: PointerEvent<HTMLElement>) => {
      const state = scrubRef.current

      if (!state) {
        return
      }

      if (event.pointerType === "mouse" && event.buttons === 0) {
        endScrubSession(event, state.source === "input")
        return
      }

      if (
        !state.scrubbing &&
        hasExceededScrubThreshold(
          event,
          state.startX,
          state.startY,
          scrub.direction,
          scrubThreshold,
        )
      ) {
        activateScrubbing(event, event.currentTarget)
      }

      if (!state.scrubbing) {
        return
      }

      const pointerDelta = getScrubPointerDelta(
        event,
        state.startX,
        state.startY,
        scrub.direction,
      )

      const modifiers = getDomStepModifiers({
        shiftKey: event.shiftKey,
        altKey: event.altKey,
        metaKey: event.metaKey,
        getModifierState: (key) => event.getModifierState(key as "Shift"),
      })
      const delta = getActiveStep(modifiers)
      const effectiveDelta = pointerDelta / scrub.sensitivity
      const attempted = applyStepDelta(
        state.startValue,
        effectiveDelta * delta,
        {
          step,
          fineStep,
          fine: modifiers.fine,
          userDecimalPlaces: userDecimalPlacesRef.current,
        },
      )
      const scrubDirection =
        effectiveDelta === 0 ? undefined : effectiveDelta > 0 ? 1 : -1
      commit(
        attempted,
        "scrub",
        resolveCommitQuantizeStep(state.startValue, modifiers.fine),
        delta,
        scrubDirection,
      )
    },
    [
      activateScrubbing,
      commit,
      endScrubSession,
      fineStep,
      getActiveStep,
      getDomStepModifiers,
      resolveCommitQuantizeStep,
      scrub.direction,
      scrub.sensitivity,
      scrubThreshold,
      step,
    ],
  )

  const beginLabelScrub = useCallback(
    (event: PointerEvent<HTMLElement>) => {
      if (!canScrub) {
        return
      }

      if (event.pointerType === "mouse" && event.button !== 0) {
        return
      }

      const current = Number(draft.replace(/^\+/, ""))

      if (!Number.isFinite(current)) {
        return
      }

      scrubRef.current = {
        captureTarget: event.currentTarget,
        pointerId: event.pointerId,
        scrubbing: false,
        source: "label",
        startValue: current,
        startX: event.clientX,
        startY: event.clientY,
      }
      attachScrubSessionGuard()
      event.preventDefault()
      event.currentTarget.setPointerCapture(event.pointerId)
    },
    [attachScrubSessionGuard, canScrub, draft],
  )

  const beginInputScrub = useCallback(
    (event: PointerEvent<HTMLElement>) => {
      if (!canScrub) {
        return
      }

      if (event.pointerType === "mouse" && event.button !== 0) {
        return
      }

      const current = Number(draft.replace(/^\+/, ""))

      if (!Number.isFinite(current)) {
        return
      }

      scrubRef.current = {
        captureTarget: null,
        pointerId: event.pointerId,
        scrubbing: false,
        source: "input",
        startValue: current,
        startX: event.clientX,
        startY: event.clientY,
      }
      attachScrubSessionGuard()
      event.preventDefault()
    },
    [attachScrubSessionGuard, canScrub, draft],
  )

  const onInputPointerMove = applyScrubDelta

  const scrubSurfaceHandlers = useMemo(
    () => ({
      onPointerCancel: (event: PointerEvent<HTMLElement>) => {
        endScrubSession(event, false)
      },
      onPointerDown: beginInputScrub,
      onPointerMove: onInputPointerMove,
      onPointerUp: (event: PointerEvent<HTMLElement>) => {
        endScrubSession(event, true)
      },
    }),
    [beginInputScrub, endScrubSession, onInputPointerMove],
  )

  const logoScrubHandlers = useMemo(
    () => ({
      onPointerCancel: (event: PointerEvent<HTMLElement>) => {
        endScrubSession(event, false)
      },
      onPointerDown: beginLabelScrub,
      onPointerMove: applyScrubDelta,
      onPointerUp: (event: PointerEvent<HTMLElement>) => {
        endScrubSession(event, false)
      },
    }),
    [applyScrubDelta, beginLabelScrub, endScrubSession],
  )

  const focusDisplaySurface = useCallback(() => {
    requestAnimationFrame(() => {
      displaySurfaceRef.current?.focus()
    })
  }, [])

  const onDisplayFocus = useCallback(() => {
    interactingRef.current = true
  }, [])

  const onDisplayBlur = useCallback(() => {
    finishInteraction()
    resetWheelAccumulator()
  }, [finishInteraction, resetWheelAccumulator])

  const activateEdit = enterEditMode

  const atBound = getAtBound(value, min, max)

  const spinbuttonProps = {
    "aria-valuemax": max,
    "aria-valuemin": min,
    "aria-valuenow": value,
    role: "spinbutton" as const,
  }

  const inputProps = {
    ...spinbuttonProps,
    "aria-invalid": invalid || undefined,
    "data-slot": "scrub-number-scrubbable",
    inputMode: "decimal" as const,
    onBlur: () => {
      interactingRef.current = false
      editingRef.current = false
      setEditing(false)

      const currentDraft = draftRef.current
      const draftBody = currentDraft.replace(/^\+/, "").trim()

      if (draftBody === "" || draftBody === "-" || draftBody === "+" || draftBody === ".") {
        setInvalid(true)
        setDraft(formatForEdit(value))
        window.setTimeout(() => {
          setInvalid(false)
        }, 600)
        return
      }

      const parsed = Number(draftBody)

      if (Number.isFinite(parsed)) {
        setInvalid(false)
        const decimalPlaces = countDraftDecimalPlaces(currentDraft)
        userDecimalPlacesRef.current =
          decimalPlaces > 0 ? decimalPlaces : null
        const bounded = commit(
          parsed,
          undefined,
          resolveCommitQuantizeStep(parsed),
        )
        notifyCommit(bounded)
        return
      }

      setInvalid(true)
      setDraft(formatForEdit(value))
      window.setTimeout(() => {
        setInvalid(false)
      }, 600)
    },
    onChange: (event: ChangeEvent<HTMLInputElement>) => {
      setInvalid(false)
      const nextValue = sanitizeNumericDraft(
        event.currentTarget.value,
        draftRef.current,
      )
      draftRef.current = nextValue
      setDraft(nextValue)
    },
    onFocus: () => {
      interactingRef.current = true
      setInvalid(false)
    },
    onKeyDown: (event: KeyboardEvent<HTMLInputElement>) => {
      if (event.key === "Enter") {
        event.currentTarget.blur()
        return
      }

      if (event.key === "Escape") {
        setInvalid(false)
        const revertedDraft = formatForEdit(value)
        draftRef.current = revertedDraft
        setDraft(revertedDraft)
        editingRef.current = false
        setEditing(false)
        event.currentTarget.blur()
        return
      }

      if (handleKeyboardNudge(event)) {
        editingRef.current = false
        event.currentTarget.blur()
        focusDisplaySurface()
        return
      }
    },
    ref: inputRef,
    type: "text" as const,
    value: draft,
  }

  const handleDisplayKeyDown = useCallback(
    (event: KeyboardEvent<HTMLElement>) => {
      if (disabled) {
        return
      }

      if (handleKeyboardNudge(event)) {
        return
      }

      if (event.key === "Enter" || event.key === " ") {
        event.preventDefault()
        enterEditMode()
      }
    },
    [disabled, enterEditMode, handleKeyboardNudge],
  )

  const visibleDisplayValue = (() => {
    if (editing) {
      return draft
    }

    const draftNumeric = Number(draft.replace(/^\+/, ""))

    if (
      Number.isFinite(draftNumeric) &&
      draftNumeric === lastCommittedValueRef.current &&
      lastCommittedValueRef.current !== value
    ) {
      return formatForDisplay(lastCommittedValueRef.current)
    }

    return formatForDisplay(value)
  })()

  return {
    activateEdit,
    atBound,
    boundFeedback,
    canScrub,
    clearBoundFeedback,
    displaySurfaceRef,
    displayValue: visibleDisplayValue,
    editing,
    handleDisplayKeyDown,
    inputProps,
    inputRef,
    interactingRef,
    interactionEpoch,
    invalid,
    logoScrubHandlers,
    logoScrollEnabled,
    nudgeTrend: lastNudgeDirectionRef.current,
    onDisplayBlur,
    onDisplayFocus,
    scrubSurfaceHandlers,
    spinbuttonProps,
    surfaceRef,
  }
}

function splitSignedDisplayValue(value: string) {
  if (value.startsWith("-")) {
    return { body: value.slice(1), sign: "-" }
  }

  if (value.startsWith("+")) {
    return { body: value.slice(1), sign: "+" }
  }

  return { body: value, sign: "" }
}

function mirrorInputTypography(source: HTMLElement): CSSProperties {
  const computed = getComputedStyle(source)

  return {
    fontFamily: computed.fontFamily,
    fontFeatureSettings: computed.fontFeatureSettings,
    fontSize: computed.fontSize,
    fontStyle: computed.fontStyle,
    fontVariantNumeric: computed.fontVariantNumeric as CSSProperties["fontVariantNumeric"],
    fontWeight: computed.fontWeight,
    letterSpacing: computed.letterSpacing,
    lineHeight: computed.lineHeight,
  }
}

function mirrorCalligraphTypography(source: HTMLElement): CSSProperties {
  return {
    ...mirrorInputTypography(source),
    lineHeight: 1,
  }
}

function CalligraphNumber({
  contentRef,
  layoutKey,
  settings = DEFAULT_CALLIGRAPH_SETTINGS,
  style,
  trend = 0,
  value,
}: {
  contentRef?: RefObject<HTMLSpanElement | null>
  layoutKey?: string
  settings?: CalligraphSettings
  style?: CSSProperties
  trend?: 1 | -1 | 0
  value: string
}) {
  const shouldReduceMotion = useReducedMotion()
  const { body, sign } = splitSignedDisplayValue(value)

  const animation =
    settings.animation === "default" ? undefined : settings.animation

  if (shouldReduceMotion) {
    return (
      <span
        ref={contentRef}
        data-slot="scrub-number-calligraph-content"
        style={style}
      >
        {value}
      </span>
    )
  }

  return (
    <span
      ref={contentRef}
      className="inline-flex items-center justify-start"
      data-slot="scrub-number-calligraph-content"
      style={style}
    >
      {sign ? (
        <span aria-hidden="true" className="inline-block" style={style}>
          {sign}
        </span>
      ) : null}
      <Calligraph
        key={layoutKey}
        animation={animation}
        autoSize={settings.autoSize}
        className="scrub-number-calligraph inline-flex items-center justify-start leading-none"
        stagger={settings.stagger}
        style={style}
        trend={trend}
        variant={settings.variant}
      >
        {body}
      </Calligraph>
    </span>
  )
}

function getFieldClasses(inputClassName?: string, extra?: string) {
  return cn(
    scrubFieldVariants(),
    SCRUB_NUMBER_FIELD_CLASS,
    SCRUB_NUMBER_SPINNER_HIDE_CLASS,
    extra,
    inputClassName,
  )
}

export function ScrubNumberInput({
  calligraph = DEFAULT_CALLIGRAPH_SETTINGS,
  className,
  disabled,
  grouped = false,
  inputClassName,
  // eslint-disable-next-line @typescript-eslint/no-unused-vars -- API parity only
  inputSettings = DEFAULT_INPUT_SETTINGS,
  logo = DEFAULT_LOGO_SETTINGS,
  max,
  min,
  scrub,
  scrubSettings = DEFAULT_SCRUB_SETTINGS,
  ...props
}: {
  calligraph?: CalligraphSettings
  className?: string
  disabled?: boolean
  grouped?: boolean
  inputClassName?: string
  inputSettings?: InputSettings
  logo?: LogoSettings
  max?: number
  min?: number
  scrub: ScrubState
  scrubSettings?: ScrubSettings
} & Omit<ComponentProps<"input">, "onChange" | "type" | "value" | "size">) {
  const scrubBounds = { min, max }
  const fieldClass = getFieldClasses(inputClassName)
  const ariaLabel = props["aria-label"]
  const mirrorRef = useRef<HTMLInputElement>(null)
  const calligraphClipRef = useRef<HTMLDivElement>(null)
  const calligraphContentRef = useRef<HTMLSpanElement>(null)
  const [mirroredTypography, setMirroredTypography] = useState<CSSProperties>({})
  const prevTypographyRef = useRef<string>("")
  const logoScrollEnabled = scrub.logoScrollEnabled
  const usesInputGroup = logoScrollEnabled
  const usesGroupedControl = grouped || logoScrollEnabled
  const isDisplayTruncated = useDisplayOverflowTruncated(
    calligraphClipRef,
    [scrub.displayValue, mirroredTypography, scrub.editing, scrub.interactionEpoch],
    mirrorRef,
  )
  const displaySpinbuttonProps = {
    ...scrub.spinbuttonProps,
    ...(isDisplayTruncated ? { "aria-valuetext": scrub.displayValue } : {}),
  }

  useLayoutEffect(() => {
    const syncMirroredTypography = () => {
      if (scrub.interactingRef.current) {
        return
      }

      const source = scrub.editing ? scrub.inputRef.current : mirrorRef.current

      if (!source) {
        return
      }

      const nextTypography = mirrorCalligraphTypography(source)
      const nextKey = JSON.stringify(nextTypography)
      const typographyChanged = nextKey !== prevTypographyRef.current

      prevTypographyRef.current = nextKey

      if (!typographyChanged) {
        return
      }

      setMirroredTypography(nextTypography)
    }

    syncMirroredTypography()

    const source = scrub.editing ? scrub.inputRef.current : mirrorRef.current

    if (!source || typeof ResizeObserver === "undefined") {
      return
    }

    const observer = new ResizeObserver(syncMirroredTypography)
    observer.observe(source)

    return () => {
      observer.disconnect()
    }
  }, [scrub.displayValue, scrub.editing, scrub.inputRef, scrub.interactingRef, scrub.interactionEpoch])

  const groupControlClass =
    "relative z-[1] flex min-w-0 w-full flex-1 items-center justify-start overflow-hidden rounded-none border-0 bg-transparent text-foreground shadow-none dark:bg-transparent"

  const calligraphLayoutKey = usesGroupedControl ? "group" : "field"

  const scrubSurface = scrub.editing ? (
    <Input
      {...props}
      {...scrub.inputProps}
      className={cn(
        fieldClass,
        usesGroupedControl
          ? "w-full rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent"
          : "relative z-[1] scrub-bound-field",
        "text-start",
      )}
      disabled={disabled}
      data-slot={usesGroupedControl ? "input-group-control" : undefined}
    />
  ) : (
    <div
      ref={scrub.displaySurfaceRef}
      {...(logoScrollEnabled ? {} : scrub.scrubSurfaceHandlers)}
      {...displaySpinbuttonProps}
      aria-label={typeof ariaLabel === "string" ? ariaLabel : undefined}
      aria-invalid={scrub.invalid || undefined}
      className={cn(
        fieldClass,
        usesGroupedControl
          ? groupControlClass
          : cn(
              "relative z-[1] flex items-center justify-start text-foreground scrub-bound-field",
            ),
        !logoScrollEnabled &&
          scrub.canScrub &&
          getScrubCursorClass(scrubSettings, scrub.atBound, scrubBounds),
        !logoScrollEnabled && scrub.canScrub && "select-none",
        logoScrollEnabled && "cursor-text",
        disabled && "cursor-not-allowed opacity-50",
        scrub.invalid && "is-bound-error",
      )}
      data-slot={usesGroupedControl ? "input-group-control" : "scrub-number-scrubbable"}
      tabIndex={disabled ? -1 : 0}
      title={isDisplayTruncated ? scrub.displayValue : undefined}
      onClick={
        logoScrollEnabled && !disabled
          ? () => {
              scrub.activateEdit()
            }
          : undefined
      }
      onBlur={scrub.onDisplayBlur}
      onFocus={scrub.onDisplayFocus}
      onKeyDown={scrub.handleDisplayKeyDown}
    >
      <motion.div
        {...(grouped ? {} : { layoutRoot: true })}
        ref={calligraphClipRef}
        className={cn(
          "pointer-events-none relative flex w-full min-w-0 items-center justify-start overflow-hidden text-foreground",
        )}
        data-slot="scrub-number-calligraph-value"
        style={mirroredTypography}
      >
        <CalligraphNumber
          contentRef={calligraphContentRef}
          layoutKey={calligraphLayoutKey}
          settings={calligraph}
          style={mirroredTypography}
          trend={scrub.nudgeTrend}
          value={scrub.displayValue}
        />
      </motion.div>
    </div>
  )

  const fieldContent = (
    <div
      className={cn(
        "relative",
        usesGroupedControl ? "flex min-w-0 flex-1 overflow-hidden" : "shrink-0",
      )}
    >
      <Input
        ref={mirrorRef}
        aria-hidden
        aria-label={
          typeof ariaLabel === "string" ? ariaLabel : "Scrub number value"
        }
        className={fieldClass}
        readOnly
        tabIndex={-1}
        value={scrub.displayValue}
        style={{
          inset: 0,
          opacity: 0,
          pointerEvents: "none",
          position: "absolute",
          zIndex: 0,
        }}
      />
      {scrubSurface}
    </div>
  )

  const inputGroup = (
    <InputGroup
      className={cn("h-7 scrub-bound-field w-full")}
      data-logo-scroll={logoScrollEnabled ? "" : undefined}
    >
      {fieldContent}
      {logoScrollEnabled ? (
        <InputGroupAddon
          align="inline-end"
          aria-label="Scrub value with logo"
          {...scrub.logoScrubHandlers}
          className={cn(
            "shrink-0 select-none pr-1.5",
            scrub.canScrub &&
              getScrubCursorClass(scrubSettings, scrub.atBound, scrubBounds),
          )}
          data-slot="scrub-number-logo-scroll"
          onClick={(event) => {
            event.preventDefault()
          }}
        >
          <ScrubLogoIcon
            className="pointer-events-none size-3.5 shrink-0 text-muted-foreground"
            name={logo.icon}
          />
        </InputGroupAddon>
      ) : null}
    </InputGroup>
  )

  return (
    <div ref={scrub.surfaceRef} className={cn("relative shrink-0", className)}>
      <ScrubBoundFeedback
        boundFeedback={scrub.boundFeedback}
        className={usesInputGroup || grouped ? "w-full min-w-0" : undefined}
        mode={scrubSettings.boundFeedback}
        onFeedbackComplete={scrub.clearBoundFeedback}
      >
        {usesInputGroup ? inputGroup : fieldContent}
      </ScrubBoundFeedback>
    </div>
  )
}

export type ScrubNumberFieldProps = Omit<
  ComponentProps<"input">,
  "onChange" | "type" | "value" | "defaultValue" | "size" | "format"
> & {
  allowWheelScrub?: boolean
  boundFeedback?: BoundFeedbackMode
  calligraph?: CalligraphSettings
  defaultResetValue?: number
  direction?: "horizontal" | "vertical"
  format?: Intl.NumberFormatOptions
  formatValue?: (value: number) => string
  grouped?: boolean
  inputSettings?: InputSettings
  label?: string
  labelClassName?: string
  largeStep?: number
  logo?: LogoSettings
  onValueChange?: (value: number) => void
  onValueCommitted?: (value: number) => void
  pixelSensitivity?: number
  smallStep?: number
  value?: number
  defaultValue?: number
  min?: number
  max?: number
  step?: number
  className?: string
  inputClassName?: string
}

export function ScrubNumberField({
  allowWheelScrub = false,
  boundFeedback = "none",
  calligraph = DEFAULT_CALLIGRAPH_SETTINGS,
  className,
  defaultResetValue,
  defaultValue,
  direction = "horizontal",
  disabled,
  format,
  formatValue: formatValueProp,
  grouped = false,
  inputSettings = DEFAULT_INPUT_SETTINGS,
  label,
  labelClassName,
  largeStep = 10,
  logo = DEFAULT_LOGO_SETTINGS,
  max,
  min,
  onValueChange,
  onValueCommitted,
  pixelSensitivity = 2,
  smallStep = 0.1,
  step = 1,
  value: valueProp,
  inputClassName,
  ...props
}: ScrubNumberFieldProps) {
  const { min: normalizedMin, max: normalizedMax } = normalizeNumberFieldBounds(
    min,
    max,
  )

  const [value, setValue] = useControllableState({
    prop: valueProp,
    defaultProp: defaultValue ?? 0,
    onChange: onValueChange,
    caller: "ScrubNumberField",
  })

  const resetValue = defaultResetValue ?? defaultValue

  const scrubSettings = flatSettingsToScrubSettings({
    allowWheelScrub,
    boundFeedback,
    direction,
    largeStep,
    pixelSensitivity,
    smallStep,
    step,
  })

  const formatValue =
    formatValueProp ??
  (format
    ? (nextValue: number) =>
        new Intl.NumberFormat(undefined, format).format(nextValue)
    : undefined)

  const scrub = useNumberScrub({
    disabled,
    format: DEFAULT_FORMAT_SETTINGS,
    formatValue,
    logo,
    max: normalizedMax,
    min: normalizedMin,
    onChange: setValue,
    onValueCommit: onValueCommitted,
    defaultResetValue: resetValue,
    scrub: scrubSettings,
    selectOnEdit: inputSettings.selectOnEdit,
    shiftStep: largeStep,
    step,
    value,
  })

  const field = (
    <div className="min-w-0">
      <ScrubNumberInput
        {...props}
        calligraph={calligraph}
        className={cn(
          grouped
            ? "min-w-0 flex-1"
            : logo.enabled
              ? "w-[6.75rem]"
              : "w-[4.75rem]",
          className,
        )}
        disabled={disabled}
        grouped={grouped}
        inputClassName={inputClassName}
        inputSettings={inputSettings}
        logo={logo}
        max={normalizedMax}
        min={normalizedMin}
        scrub={scrub}
        scrubSettings={scrubSettings}
      />
    </div>
  )

  if (!label) {
    return field
  }

  return (
    <div className="flex items-center gap-3">
      <span
        className={cn(
          "w-16 text-sm font-medium text-muted-foreground",
          labelClassName,
        )}
      >
        {label}
      </span>
      {field}
    </div>
  )
}
components/ui/scrub-number-input.css
[data-slot="scrub-number-calligraph-value"],
[data-slot="scrub-number-calligraph-value"] .scrub-number-calligraph {
  line-height: 1;
  overflow: hidden;
}

[data-slot="scrub-number-calligraph-value"] {
  color: inherit;
  display: flex;
  justify-content: flex-start;
  position: relative;
}

[data-slot="scrub-number-calligraph-value"] .scrub-number-calligraph,
[data-slot="scrub-number-calligraph-value"] .scrub-number-calligraph * {
  color: inherit;
}

[data-slot="scrub-number-calligraph-value"] .scrub-number-calligraph {
  align-items: center !important;
  justify-content: flex-start !important;
}

[data-slot="scrub-number-calligraph-value"] .scrub-number-calligraph > span {
  align-items: center !important;
  display: inline-flex !important;
  justify-content: flex-start !important;
  margin: 0 !important;
  overflow: hidden !important;
  padding: 0 !important;
}

[data-slot="scrub-number-calligraph-value"]
  .scrub-number-calligraph
  span[style*="vertical-align: top"] {
  align-items: center !important;
  display: inline-flex !important;
  height: 1em !important;
  justify-content: flex-start !important;
  line-height: 1 !important;
  max-height: 1em !important;
  overflow: hidden !important;
  vertical-align: middle !important;
}

[data-slot="scrub-number-calligraph-value"]
  .scrub-number-calligraph
  span[style*="visibility: hidden"] {
  font: inherit !important;
  line-height: inherit !important;
}

[data-slot="scrub-number-scrubbable"],
[data-slot="input-group-control"] {
  text-align: start;
}

:root {
  --scrub-bound-shake-distance: 6px;
  --scrub-bound-shake-overshoot: 4px;
  --scrub-bound-shake-dur-a: 80ms;
  --scrub-bound-shake-dur-b: 60ms;
  --scrub-bound-shake-ease: cubic-bezier(0.22, 1, 0.36, 1);
  --scrub-bound-feedback-ms: calc(
    var(--scrub-bound-shake-dur-a) * 2 + var(--scrub-bound-shake-dur-b) * 2
  );
  --scrub-bound-revert-hold: 600ms;
  --scrub-bound-revert-dur: 280ms;
  --scrub-bound-shake-color: var(--foreground);
  --scrub-number-field-padding-inline: 0.5rem;
}

@keyframes scrub-bound-pulse {
  0% {
    box-shadow: 0 0 0 0 color-mix(in oklch, var(--scrub-bound-shake-color) 60%, transparent);
  }

  100% {
    box-shadow: 0 0 0 3px color-mix(in oklch, var(--scrub-bound-shake-color) 0%, transparent);
  }
}

[data-slot="scrub-bound-feedback"][data-bound-hit] [data-slot="input-group"],
[data-slot="scrub-bound-feedback"][data-bound-hit] [data-slot="scrub-number-scrubbable"],
[data-slot="scrub-bound-feedback"][data-bound-hit] [data-slot="input-group-control"] {
  animation: scrub-bound-pulse var(--scrub-bound-feedback-ms) linear;
}

@media (prefers-reduced-motion: reduce) {
  [data-slot="scrub-bound-feedback"][data-bound-hit] [data-slot="input-group"],
  [data-slot="scrub-bound-feedback"][data-bound-hit] [data-slot="scrub-number-scrubbable"],
  [data-slot="scrub-bound-feedback"][data-bound-hit] [data-slot="input-group-control"] {
    animation: none;
    box-shadow: 0 0 0 2px var(--scrub-bound-shake-color);
  }
}

.scrub-bound-field:not([data-slot="input-group"]),
[data-slot="scrub-number-scrubbable"],
[data-slot="input-group-control"],
[data-slot="scrub-bound-feedback"] [data-slot="input-group"] [data-slot="input-group-control"] {
  padding-inline: var(--scrub-number-field-padding-inline) !important;
}

.scrub-bound-field {
  transition: border-color 150ms ease-out;
  will-change: transform;
}

.scrub-bound-wrap.is-bound-error .scrub-bound-field {
  border-color: var(--scrub-bound-shake-color);
  transition: border-color var(--scrub-bound-revert-dur) ease-out;
}

.scrub-bound-field.is-shaking {
  animation: scrub-bound-shake-x var(--scrub-bound-feedback-ms) linear;
}

.scrub-bound-wrap[data-bound-direction="vertical"] .scrub-bound-field.is-shaking {
  animation-name: scrub-bound-shake-y;
}

@keyframes scrub-bound-shake-x {
  0% {
    transform: translateX(0);
    animation-timing-function: var(--scrub-bound-shake-ease);
  }

  28.57% {
    transform: translateX(var(--scrub-bound-shake-distance));
    animation-timing-function: var(--scrub-bound-shake-ease);
  }

  57.14% {
    transform: translateX(calc(var(--scrub-bound-shake-distance) * -1));
    animation-timing-function: var(--scrub-bound-shake-ease);
  }

  78.57% {
    transform: translateX(var(--scrub-bound-shake-overshoot));
    animation-timing-function: var(--scrub-bound-shake-ease);
  }

  100% {
    transform: translateX(0);
  }
}

@keyframes scrub-bound-shake-y {
  0% {
    transform: translateY(0);
    animation-timing-function: var(--scrub-bound-shake-ease);
  }

  28.57% {
    transform: translateY(var(--scrub-bound-shake-distance));
    animation-timing-function: var(--scrub-bound-shake-ease);
  }

  57.14% {
    transform: translateY(calc(var(--scrub-bound-shake-distance) * -1));
    animation-timing-function: var(--scrub-bound-shake-ease);
  }

  78.57% {
    transform: translateY(var(--scrub-bound-shake-overshoot));
    animation-timing-function: var(--scrub-bound-shake-ease);
  }

  100% {
    transform: translateY(0);
  }
}

@media (prefers-reduced-motion: reduce) {
  .scrub-bound-field.is-shaking {
    animation: none !important;
    transform: none !important;
  }
}
lib/scrub-number-overflow.ts
import { useLayoutEffect, useState, type RefObject } from "react"

function measureContentOverflow(
  container: HTMLElement | null,
  content: HTMLElement | null,
) {
  if (!container || !content) {
    return false
  }

  const contentOverflows =
    content.scrollWidth > content.clientWidth + 1 ||
    content.getBoundingClientRect().width > container.clientWidth + 1

  if (!contentOverflows) {
    return false
  }

  return content.scrollWidth > container.clientWidth + 1
}

export function useDisplayOverflowTruncated(
  containerRef: RefObject<HTMLElement | null>,
  deps: ReadonlyArray<unknown>,
  contentRef?: RefObject<HTMLElement | null>,
) {
  const [isTruncated, setIsTruncated] = useState(false)

  useLayoutEffect(() => {
    const container = containerRef.current
    const content = contentRef?.current ?? container

    if (!container || !content) {
      setIsTruncated(false)
      return
    }

    const update = () => {
      setIsTruncated(measureContentOverflow(container, content))
    }

    update()

    if (typeof ResizeObserver === "undefined") {
      return
    }

    const observer = new ResizeObserver(update)
    observer.observe(container)
    if (content !== container) {
      observer.observe(content)
    }

    return () => {
      observer.disconnect()
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps -- remeasure when display content changes
  }, deps)

  return isTruncated
}
lib/scrub-number-math.ts
export type DisplayFormat = {
  alwaysShowSign: boolean
}

export type ResolveActiveStepOptions = {
  step: number
  shiftStep: number
  fineStep: number
  coarse?: boolean
  fine?: boolean
}

export type ModifierKey = "shift" | "alt" | "meta"

export type FineModifier = "shift" | "alt" | "meta"

export type CoarseModifier = "shift" | "alt" | "meta"

const FINE_MODIFIER_KEYS = new Set<FineModifier>(["shift", "alt", "meta"])
const COARSE_MODIFIER_KEYS = new Set<CoarseModifier>(["shift", "alt", "meta"])

export type ModifierKeys = {
  shiftKey: boolean
  altKey: boolean
  metaKey: boolean
  getModifierState?: (key: string) => boolean
}

const DEFAULT_SCRUB_THRESHOLD = 3

export function isMacPlatform() {
  if (typeof navigator === "undefined") {
    return false
  }

  const platform = navigator.platform ?? ""
  const userAgent = navigator.userAgent ?? ""

  return /Mac|iPhone|iPad|iPod/.test(platform) || /Mac OS X/.test(userAgent)
}

export function isModifierKeyPressed(event: ModifierKeys, key: ModifierKey) {
  const getState = event.getModifierState?.bind(event)

  switch (key) {
    case "shift":
      return event.shiftKey || getState?.("Shift") === true
    case "alt":
      return event.altKey || getState?.("Alt") === true
    case "meta":
      return event.metaKey || getState?.("Meta") === true
  }
}

export function normalizeFineModifier(
  modifier: string | undefined,
  fallback: FineModifier = "alt",
): FineModifier {
  if (modifier != null && FINE_MODIFIER_KEYS.has(modifier as FineModifier)) {
    return modifier as FineModifier
  }

  if (modifier === "auto") {
    return isMacPlatform() ? "meta" : "alt"
  }

  return fallback
}

export function normalizeCoarseModifier(
  modifier: string | undefined,
  fallback: CoarseModifier = "shift",
): CoarseModifier {
  if (modifier != null && COARSE_MODIFIER_KEYS.has(modifier as CoarseModifier)) {
    return modifier as CoarseModifier
  }

  if (modifier === "auto") {
    return "shift"
  }

  return fallback
}

export const MODIFIER_OPTIONS = ["shift", "alt", "meta"] as const satisfies readonly ModifierKey[]

export function pickAlternateModifier(
  current: ModifierKey,
  exclude: ModifierKey,
): ModifierKey {
  return MODIFIER_OPTIONS.find((option) => option !== exclude) ?? current
}

export function resolveExclusiveModifiers(
  fine: FineModifier,
  coarse: CoarseModifier,
): { fine: FineModifier; coarse: CoarseModifier } {
  if (fine !== coarse) {
    return { fine, coarse }
  }

  return {
    fine,
    coarse: pickAlternateModifier(coarse, fine) as CoarseModifier,
  }
}

export function resolveFineModifierKey(
  modifier: FineModifier = "alt",
): ModifierKey {
  return modifier
}

export function resolveCoarseModifierKey(
  modifier: CoarseModifier = "shift",
): ModifierKey {
  return modifier
}

export function isFineModifierPressed(
  event: ModifierKeys,
  modifier: FineModifier = "alt",
) {
  return isModifierKeyPressed(event, resolveFineModifierKey(modifier))
}

export function isCoarseModifierPressed(
  event: ModifierKeys,
  modifier: CoarseModifier = "shift",
) {
  return isModifierKeyPressed(event, resolveCoarseModifierKey(modifier))
}

export function getModifierLabel(key: ModifierKey) {
  switch (key) {
    case "shift":
      return "Shift"
    case "alt":
      return "Alt"
    case "meta":
      return "Cmd"
  }
}

export function getFineModifierLabel(modifier: FineModifier = "alt") {
  return getModifierLabel(resolveFineModifierKey(modifier))
}

export function getCoarseModifierLabel(modifier: CoarseModifier = "shift") {
  return getModifierLabel(resolveCoarseModifierKey(modifier))
}

export type ScrubStepModifiers = {
  coarse: boolean
  fine: boolean
}

export function toModifierKeys(event: {
  shiftKey: boolean
  altKey: boolean
  metaKey: boolean
  getModifierState?: (key: string) => boolean
}): ModifierKeys {
  return {
    shiftKey: event.shiftKey,
    altKey: event.altKey,
    metaKey: event.metaKey,
    getModifierState: event.getModifierState
      ? (key) => event.getModifierState!(key)
      : undefined,
  }
}

export function resolveScrubStepModifiers(
  event: ModifierKeys,
  options: {
    fineModifier?: FineModifier
    coarseModifier?: CoarseModifier
  } = {},
): ScrubStepModifiers {
  const coarse = isCoarseModifierPressed(event, options.coarseModifier)
  const fine = isFineModifierPressed(event, options.fineModifier)

  return { coarse, fine }
}

export function clampNumber(value: number, min?: number, max?: number) {
  let bounded = value

  if (min != null && Number.isFinite(min)) {
    bounded = Math.max(min, bounded)
  }

  if (max != null && Number.isFinite(max)) {
    bounded = Math.min(max, bounded)
  }

  return bounded
}

export function normalizeFiniteNumber(
  value: number | undefined,
): number | undefined {
  return typeof value === "number" && Number.isFinite(value) ? value : undefined
}

export function normalizeNumberFieldBounds(
  min?: number,
  max?: number,
): { min?: number; max?: number } {
  let normalizedMin = normalizeFiniteNumber(min)
  let normalizedMax = normalizeFiniteNumber(max)

  if (
    normalizedMin != null &&
    normalizedMax != null &&
    normalizedMin > normalizedMax
  ) {
    ;[normalizedMin, normalizedMax] = [normalizedMax, normalizedMin]
  }

  return { min: normalizedMin, max: normalizedMax }
}

export function normalizePositiveFiniteStep(
  step: number | undefined,
  fallback = 1,
) {
  if (
    typeof step === "number" &&
    Number.isFinite(step) &&
    step > 0
  ) {
    return step
  }

  return fallback
}

export function normalizeWheelSensitivity(
  sensitivity: number | undefined,
  fallback = 20,
) {
  return clampNumber(
    normalizePositiveFiniteStep(sensitivity, fallback),
    1,
    200,
  )
}

export function normalizeScrubThreshold(threshold: number | undefined, fallback = 3) {
  const candidate =
    typeof threshold === "number" && Number.isFinite(threshold)
      ? threshold
      : fallback

  return clampNumber(candidate, 1, 20)
}

export function getBoundEdge(
  current: number,
  attempted: number,
  min?: number,
  max?: number,
): "min" | "max" | null {
  if (attempted > current && max != null && current >= max) {
    return "max"
  }

  if (attempted < current && min != null && current <= min) {
    return "min"
  }

  return null
}

export function getAtBound(
  value: number,
  min?: number,
  max?: number,
): "min" | "max" | null {
  if (max != null && value >= max) {
    return "max"
  }

  if (min != null && value <= min) {
    return "min"
  }

  return null
}

export function boundOverflow(
  attempted: number,
  edge: "min" | "max",
  min?: number,
  max?: number,
) {
  if (edge === "max" && max != null) {
    return Math.max(0, attempted - max)
  }

  if (edge === "min" && min != null) {
    return Math.max(0, min - attempted)
  }

  return 1
}

export function quantizeNumber(value: number, step: number) {
  if (!Number.isFinite(step) || step <= 0) {
    return value
  }

  const quantized = Math.round(value / step) * step

  if (Number.isInteger(step)) {
    return quantized
  }

  const decimals = step.toString().split(".")[1]?.length ?? 0
  return parseFloat(quantized.toFixed(decimals))
}

export function getDecimalPlaces(step: number) {
  if (!Number.isFinite(step) || Number.isInteger(step)) {
    return 0
  }

  return step.toString().split(".")[1]?.length ?? 0
}

function toPlainNumberString(value: number) {
  if (!Number.isFinite(value)) {
    return String(value)
  }

  if (Object.is(value, -0)) {
    return "0"
  }

  const str = value.toString()

  if (!/[eE]/.test(str)) {
    return str
  }

  return value.toLocaleString("en-US", {
    useGrouping: false,
    maximumFractionDigits: 100,
  })
}

export function formatMinimalDisplayValue(value: number) {
  const normalized = Number(value.toPrecision(12))

  if (Number.isInteger(normalized)) {
    return toPlainNumberString(Math.trunc(normalized))
  }

  return toPlainNumberString(normalized)
}

export function countDraftDecimalPlaces(draft: string) {
  const body = draft.trim().replace(/^[+-]/, "")

  if (!body.includes(".")) {
    return 0
  }

  return body.split(".")[1]?.length ?? 0
}

export function resolveDisplayDecimalPlaces(
  previousDraft: string,
  userDecimalPlaces?: number | null,
  activeStep?: number,
) {
  if (userDecimalPlaces != null && userDecimalPlaces > 0) {
    return userDecimalPlaces
  }

  const fromDraft = countDraftDecimalPlaces(previousDraft)

  if (fromDraft > 0) {
    return fromDraft
  }

  const fromStep = activeStep != null ? getDecimalPlaces(activeStep) : 0

  return fromStep > 0 ? fromStep : null
}

export function formatDisplayValue(
  value: number,
  format: Pick<DisplayFormat, "alwaysShowSign">,
  userDecimalPlaces: number | null = null,
) {
  const formatted =
    userDecimalPlaces != null && userDecimalPlaces > 0
      ? value.toFixed(userDecimalPlaces)
      : formatMinimalDisplayValue(value)

  if (format.alwaysShowSign && value > 0) {
    return `+${formatted}`
  }

  return formatted
}

export function resolveActiveStep(options: ResolveActiveStepOptions) {
  const { step, shiftStep, fineStep, coarse = false, fine = false } = options

  if (coarse) {
    return shiftStep
  }

  if (fine) {
    return fineStep
  }

  return step
}

const DEFAULT_WHEEL_LINE_DELTA = 16
const DEFAULT_WHEEL_PAGE_DELTA = 100

export function normalizeWheelDelta(deltaY: number, deltaMode = 0) {
  switch (deltaMode) {
    case 1:
      return deltaY * DEFAULT_WHEEL_LINE_DELTA
    case 2:
      return deltaY * DEFAULT_WHEEL_PAGE_DELTA
    default:
      return deltaY
  }
}

export type ConsumeWheelDeltaResult = {
  accumulated: number
  direction: 0 | 1 | -1
  steps: number
}

export function consumeWheelDelta(
  accumulated: number,
  deltaY: number,
  sensitivity: number,
): ConsumeWheelDeltaResult {
  const next = accumulated + deltaY
  const abs = Math.abs(next)
  const threshold = Math.max(1, sensitivity)

  if (abs < threshold) {
    return { accumulated: next, steps: 0, direction: 0 }
  }

  const steps = Math.floor(abs / threshold)
  const sign = next < 0 ? -1 : 1
  const remainder = next - sign * steps * threshold
  const direction = (next < 0 ? 1 : -1) as 1 | -1

  return { accumulated: remainder, steps, direction }
}

export function getValueDecimalPlaces(value: number) {
  if (!Number.isFinite(value)) {
    return 0
  }

  const normalized = Number(value.toPrecision(12))

  if (Number.isInteger(normalized)) {
    return 0
  }

  const text = normalized.toString()

  if (text.includes("e") || text.includes("E")) {
    return getValueDecimalPlaces(Number(normalized.toFixed(12)))
  }

  return text.split(".")[1]?.length ?? 0
}

export function stepFromDecimalPlaces(decimals: number) {
  if (decimals <= 0) {
    return 1
  }

  return Number((1 / 10 ** decimals).toFixed(decimals))
}

export function resolveQuantizeStep(options: {
  step: number
  fineStep: number
  fine?: boolean
  currentValue: number
  userDecimalPlaces?: number | null
}) {
  const activeStep = options.fine ? options.fineStep : options.step
  const valuePrecision = Math.max(
    getValueDecimalPlaces(options.currentValue),
    options.userDecimalPlaces ?? 0,
  )
  const stepPrecision = getDecimalPlaces(activeStep)

  if (valuePrecision > stepPrecision) {
    return stepFromDecimalPlaces(valuePrecision)
  }

  return activeStep
}

export function applyStepDelta(
  current: number,
  delta: number,
  options: {
    step: number
    fineStep: number
    fine?: boolean
    userDecimalPlaces?: number | null
  },
) {
  const next = current + delta

  if (options.userDecimalPlaces != null && options.userDecimalPlaces > 0) {
    return parseFloat(next.toFixed(options.userDecimalPlaces))
  }

  const quantizeStep = resolveQuantizeStep({
    step: options.step,
    fineStep: options.fineStep,
    fine: options.fine,
    currentValue: current,
    userDecimalPlaces: options.userDecimalPlaces,
  })

  return quantizeNumber(next, quantizeStep)
}

export function preserveDisplayDraft(
  currentDraft: string,
  value: number,
  fallback: string,
) {
  const parsed = Number(currentDraft.replace(/^\+/, ""))

  if (currentDraft !== "" && Number.isFinite(parsed) && parsed === value) {
    return currentDraft
  }

  return fallback
}

export function getScrubPointerDelta(
  event: { clientX: number; clientY: number },
  startX: number,
  startY: number,
  direction: "horizontal" | "vertical",
) {
  if (direction === "vertical") {
    return startY - event.clientY
  }

  return event.clientX - startX
}

export function hasExceededScrubThreshold(
  event: { clientX: number; clientY: number },
  startX: number,
  startY: number,
  direction: "horizontal" | "vertical",
  threshold = DEFAULT_SCRUB_THRESHOLD,
) {
  const effectiveThreshold = Math.max(1, threshold)

  if (direction === "vertical") {
    return Math.abs(event.clientY - startY) > effectiveThreshold
  }

  return Math.abs(event.clientX - startX) > effectiveThreshold
}

export function resolveFineStep(step: number, fineStep?: number) {
  if (fineStep != null && Number.isFinite(fineStep) && fineStep > 0) {
    return fineStep
  }

  return quantizeNumber(step / 10, step) || step / 10
}

export function sanitizeNumericDraft(value: string, previousValue = "") {
  if (value === "") {
    return ""
  }

  if (/^[+-]{2,}/.test(value)) {
    return previousValue
  }

  if (/[eE]/.test(value) || (value.match(/\./g)?.length ?? 0) > 1) {
    return previousValue
  }

  let sign = ""
  let rest = value

  if (rest.startsWith("+") || rest.startsWith("-")) {
    sign = rest[0]
    rest = rest.slice(1)
  }

  let hasDot = false
  let body = ""

  for (const character of rest) {
    if (character >= "0" && character <= "9") {
      body += character
      continue
    }

    if (character === "." && !hasDot) {
      hasDot = true
      body += character
    }
  }

  const sanitized = sign + body

  return sanitized
}
hooks/use-controllable-state.tsx
import * as React from "react"

const useLayoutEffect = globalThis?.document ? React.useLayoutEffect : () => {}

const useInsertionEffect: typeof useLayoutEffect =
  (React as never)[" useInsertionEffect ".trim().toString()] || useLayoutEffect

type ChangeHandler<T> = (state: T) => void
type SetStateFn<T> = React.Dispatch<React.SetStateAction<T>>

interface UseControllableStateParams<T> {
  prop?: T | undefined
  defaultProp: T
  onChange?: ChangeHandler<T>
  caller?: string
}

export function useControllableState<T>({
  prop,
  defaultProp,
  onChange = () => {},
  caller,
}: UseControllableStateParams<T>): [T, SetStateFn<T>] {
  const [uncontrolledProp, setUncontrolledProp, onChangeRef] =
    useUncontrolledState({
      defaultProp,
      onChange,
    })
  const isControlled = prop !== undefined
  const value = isControlled ? prop : uncontrolledProp

  /* eslint-disable react-hooks/rules-of-hooks */
  if (process.env.NODE_ENV !== "production") {
    const isControlledRef = React.useRef(prop !== undefined)
    React.useEffect(() => {
      const wasControlled = isControlledRef.current
      if (wasControlled !== isControlled) {
        const from = wasControlled ? "controlled" : "uncontrolled"
        const to = isControlled ? "controlled" : "uncontrolled"
        console.warn(
          `${caller} is changing from ${from} to ${to}. Components should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled value for the lifetime of the component.`
        )
      }
      isControlledRef.current = isControlled
    }, [isControlled, caller])
  }
  /* eslint-enable react-hooks/rules-of-hooks */

  const setValue = React.useCallback<SetStateFn<T>>(
    (nextValue) => {
      if (isControlled) {
        const value = isFunction(nextValue) ? nextValue(prop) : nextValue
        if (value !== prop) {
          onChangeRef.current?.(value)
        }
      } else {
        setUncontrolledProp(nextValue)
      }
    },
    [isControlled, prop, setUncontrolledProp, onChangeRef]
  )

  return [value, setValue]
}

function useUncontrolledState<T>({
  defaultProp,
  onChange,
}: Omit<UseControllableStateParams<T>, "prop">): [
  Value: T,
  setValue: React.Dispatch<React.SetStateAction<T>>,
  OnChangeRef: React.RefObject<ChangeHandler<T> | undefined>,
] {
  const [value, setValue] = React.useState(defaultProp)
  const prevValueRef = React.useRef(value)

  const onChangeRef = React.useRef(onChange)
  useInsertionEffect(() => {
    onChangeRef.current = onChange
  }, [onChange])

  React.useEffect(() => {
    if (prevValueRef.current !== value) {
      onChangeRef.current?.(value)
      prevValueRef.current = value
    }
  }, [value, prevValueRef])

  return [value, setValue, onChangeRef]
}

function isFunction(value: unknown): value is (...args: never[]) => unknown {
  return typeof value === "function"
}