Kinetic

MITDemoGitHub

Drag-to-scrub number fields with animated digits. Click to edit, keyboard to nudge — design-tool ergonomics for React.

Installation

pnpm dlx shadcn@latest add @kinetic/scrub-number-field

Add the @kinetic registry to components.json (until listed in the shadcn registry directory), plus shadcn input and input-group.

{
  "registries": {
    "@kinetic": "/r/{name}.json"
  }
}
  1. Install dependencies

    pnpm add calligraph lucide-react motion
  2. Add shadcn components

    pnpm dlx shadcn@latest add input input-group
  3. Copy source files

    /**
     * Scrub Number Input — self-contained copy-paste module
     *
     * Horizontal scrub + click-to-edit number field with Calligraph slot-digit animation.
     *
     * ## Display overflow
     *
     * Values stay left-aligned; overflow is clipped on the right. Hover shows the full
     * value via `title` when truncated; screen readers get `aria-valuetext`. Click-to-edit
     * Click-to-edit opens the full string. When the value overflows horizontally,
     * the input scrolls to keep the caret visible instead of forcing it to the end.
     */
    "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,
      Percent,
      type LucideIcon,
    } from "lucide-react"
    import { useReducedMotion, animate, motion, useMotionValue } 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 { spring } from "@/lib/springs"
    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-[20px] 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"
      | "rubberBand"
      | "shake"
      | "borderPulse"
    
    export const BOUND_FEEDBACK_MODES = [
      "none",
      "rubberBand",
      "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
      /** Step used while the fine modifier is held. Defaults to step / 10. */
      fineStep?: number
      /**
       * Key held for fine step.
       */
      fineModifier?: FineModifier
      /**
       * Key held for coarse step.
       */
      coarseModifier?: CoarseModifier
      /** Accumulated wheel delta (px) required before one step fires. */
      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",
      "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,
      Percent,
    }
    
    export function ScrubLogoIcon({
      className,
      name,
    }: {
      className?: string
      name: LogoIconName
    }) {
      const Icon = LOGO_ICONS[name]
      return <Icon aria-hidden className={className} />
    }
    
    export type ScrubFieldSettings = {
      scrub: ScrubSettings
      calligraph: CalligraphSettings
      input: InputSettings
      format: FormatSettings
      logo: LogoSettings
      min?: number
      max?: number
      step?: number
    }
    
    export const DEFAULT_SCRUB_FIELD_SETTINGS: ScrubFieldSettings = {
      scrub: DEFAULT_SCRUB_SETTINGS,
      calligraph: DEFAULT_CALLIGRAPH_SETTINGS,
      input: DEFAULT_INPUT_SETTINGS,
      format: DEFAULT_FORMAT_SETTINGS,
      logo: DEFAULT_LOGO_SETTINGS,
    }
    
    export function normalizeScrubFieldSettings(
      settings: ScrubFieldSettings,
    ): ScrubFieldSettings {
      const { min, max } = normalizeNumberFieldBounds(settings.min, settings.max)
    
      const resolvedStep = normalizePositiveFiniteStep(settings.step)
      const scrub = { ...DEFAULT_SCRUB_SETTINGS, ...settings.scrub }
    
      let fineStep = scrub.fineStep
      if (fineStep != null && Number.isFinite(fineStep) && fineStep > 0) {
        if (fineStep >= resolvedStep) {
          fineStep = resolveFineStep(resolvedStep)
        }
      }
    
      let shiftStep = scrub.shiftStep
      if (!Number.isFinite(shiftStep) || shiftStep < resolvedStep) {
        shiftStep = Math.max(resolvedStep, DEFAULT_SCRUB_SETTINGS.shiftStep)
      }
    
      let threshold = scrub.threshold ?? DEFAULT_SCRUB_SETTINGS.threshold ?? 3
      threshold = normalizeScrubThreshold(threshold)
    
      const fineModifier = normalizeFineModifier(
        scrub.fineModifier,
        DEFAULT_SCRUB_SETTINGS.fineModifier,
      )
    
      const coarseModifier = normalizeCoarseModifier(
        scrub.coarseModifier,
        DEFAULT_SCRUB_SETTINGS.coarseModifier,
      )
    
      const exclusiveModifiers = resolveExclusiveModifiers(
        fineModifier,
        coarseModifier,
      )
    
      let wheelSensitivity =
        scrub.wheelSensitivity ?? DEFAULT_SCRUB_SETTINGS.wheelSensitivity ?? 20
      wheelSensitivity = normalizeWheelSensitivity(wheelSensitivity)
    
      const inputOverrides = settings.input ?? {}
    
      const requestedLogoIcon = settings.logo?.icon
      const logoIcon =
        requestedLogoIcon &&
        LOGO_ICON_OPTIONS.includes(requestedLogoIcon as LogoIconName)
          ? requestedLogoIcon
          : DEFAULT_LOGO_SETTINGS.icon
    
      return {
        scrub: {
          ...scrub,
          fineStep,
          fineModifier: exclusiveModifiers.fine,
          coarseModifier: exclusiveModifiers.coarse,
          shiftStep,
          threshold,
          wheelSensitivity,
          boundFeedback:
            scrub.boundFeedback === "rubberBand"
              ? "none"
              : (scrub.boundFeedback as string) === "combo"
                ? "shake"
                : scrub.boundFeedback,
        },
        calligraph: { ...DEFAULT_CALLIGRAPH_SETTINGS, ...settings.calligraph },
        input: {
          ...DEFAULT_INPUT_SETTINGS,
          ...inputOverrides,
        },
        format: {
          alwaysShowSign: Boolean(settings.format?.alwaysShowSign),
        },
        logo: {
          ...DEFAULT_LOGO_SETTINGS,
          ...settings.logo,
          icon: logoIcon,
        },
        min,
        max,
        step:
          typeof settings.step === "number" && Number.isFinite(settings.step)
            ? settings.step
            : undefined,
      }
    }
    
    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 RUBBER_BAND_MAX_OFFSET = 6
    
    // Keep in sync with --scrub-bound-feedback-ms in scrub-number-input.css
    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,
      direction,
      mode,
      onFeedbackComplete,
    }: {
      boundFeedback: BoundFeedbackState | null
      children: React.ReactNode
      className?: string
      direction: ScrubSettings["direction"]
      mode: BoundFeedbackMode
      onFeedbackComplete: () => void
    }) {
      const shouldReduceMotion = useReducedMotion()
      const offsetX = useMotionValue(0)
      const offsetY = useMotionValue(0)
      const wrapRef = useRef<HTMLDivElement>(null)
      const [boundHit, setBoundHit] = useState<"min" | "max" | null>(null)
      const [boundError, setBoundError] = useState(false)
      const scrubbingAtEdgeRef = useRef(false)
    
      useEffect(() => {
        if (!boundFeedback || mode === "none") {
          scrubbingAtEdgeRef.current = false
          offsetX.set(0)
          offsetY.set(0)
          setBoundError(false)
          return
        }
    
        const activeMode = mode
        const sign = boundFeedback.edge === "min" ? -1 : 1
        const axis = direction === "vertical" ? offsetY : offsetX
        const otherAxis = direction === "vertical" ? offsetX : offsetY
        const shakeTargets = wrapRef.current
          ? Array.from(
              wrapRef.current.querySelectorAll<HTMLElement>(".scrub-bound-field"),
            )
          : []
    
        if (activeMode === "rubberBand") {
          const stretch = Math.min(
            RUBBER_BAND_MAX_OFFSET,
            2 + Math.sqrt(boundFeedback.overflow) * 1.25,
          )
          const offset = sign * stretch
    
          if (boundFeedback.source === "scrub") {
            scrubbingAtEdgeRef.current = true
            axis.set(shouldReduceMotion ? 0 : offset)
            otherAxis.set(0)
            return
          }
    
          scrubbingAtEdgeRef.current = false
    
          if (shouldReduceMotion) {
            axis.set(0)
            otherAxis.set(0)
            onFeedbackComplete()
            return
          }
    
          const controls = animate(axis, [offset, 0], {
            ...spring.fast,
            onComplete: onFeedbackComplete,
          })
    
          return () => {
            controls.stop()
          }
        }
    
        scrubbingAtEdgeRef.current = false
        axis.set(0)
        otherAxis.set(0)
    
        if (activeMode === "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 (activeMode === "borderPulse") {
          setBoundHit(boundFeedback.edge)
          const timeout = window.setTimeout(() => {
            setBoundHit(null)
            onFeedbackComplete()
          }, shouldReduceMotion ? 0 : SCRUB_BOUND_FEEDBACK_MS)
    
          return () => {
            window.clearTimeout(timeout)
          }
        }
      }, [
        boundFeedback,
        direction,
        mode,
        offsetX,
        offsetY,
        onFeedbackComplete,
        shouldReduceMotion,
      ])
    
      return (
        <motion.div className={className} style={{ x: offsetX, y: offsetY }}>
          <div
            ref={wrapRef}
            data-slot="scrub-bound-feedback"
            data-bound-direction={direction}
            data-bound-hit={boundHit ?? undefined}
            className={cn(
              "scrub-bound-wrap",
              boundError && "is-bound-error",
            )}
          >
            {children}
          </div>
        </motion.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
          }
    
          const isContinuousScrubRubberBand =
            source === "scrub" && scrub.boundFeedback === "rubberBand"
    
          if (!isContinuousScrubRubberBand && boundFeedbackLatchedRef.current[edge]) {
            return
          }
    
          if (!isContinuousScrubRubberBand) {
            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 {
              // Pointer capture may already be released.
            }
          }
    
          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 (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 {
            // Pointer capture may be unavailable.
          }
        },
        [],
      )
    
      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 {
      const computed = getComputedStyle(source)
      const { lineHeight: _lineHeight, ...typography } = mirrorInputTypography(source)
    
      return {
        ...typography,
        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,
      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 } : {}),
      }
    
      const syncMirroredTypography = useCallback(() => {
        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)
      }, [scrub.editing, scrub.inputRef, scrub.interactingRef])
    
      useLayoutEffect(() => {
        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.interactionEpoch, syncMirroredTypography])
    
      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
            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}
        >
          {logoScrollEnabled ? (
            <InputGroupAddon
              align="inline-start"
              aria-hidden
              className="pointer-events-none invisible shrink-0 select-none pl-1.5"
              tabIndex={-1}
            >
              <ScrubLogoIcon className="size-3.5 shrink-0" name={logo.icon} />
            </InputGroupAddon>
          ) : null}
          {fieldContent}
          {logoScrollEnabled ? (
            <InputGroupAddon
              align="inline-end"
              {...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}
            direction={scrubSettings.direction}
            mode={scrubSettings.boundFeedback}
            onFeedbackComplete={scrub.clearBoundFeedback}
          >
            {usesInputGroup ? inputGroup : fieldContent}
          </ScrubBoundFeedback>
        </div>
      )
    }
    
    export type ScrubNumberFieldProps = Omit<
      ComponentProps<"input">,
      "onChange" | "type" | "value" | "defaultValue" | "size"
    > & {
      calligraph?: CalligraphSettings
      format?: FormatSettings
      formatValue?: (value: number) => string
      inputSettings?: InputSettings
      label?: string
      labelClassName?: string
      logo?: LogoSettings
      onValueChange?: (value: number) => void
      onValueCommit?: (value: number) => void
      scrub?: ScrubSettings
      shiftStep?: number
      grouped?: boolean
      value?: number
      defaultValue?: number
      defaultResetValue?: number
      min?: number
      max?: number
      step?: number
      className?: string
      inputClassName?: string
    }
    
    export function ScrubNumberField({
      calligraph,
      className,
      defaultResetValue,
      defaultValue,
      disabled,
      format = DEFAULT_FORMAT_SETTINGS,
      formatValue,
      grouped = false,
      inputSettings = DEFAULT_INPUT_SETTINGS,
      label,
      labelClassName,
      logo = DEFAULT_LOGO_SETTINGS,
      max,
      min,
      onValueChange,
      onValueCommit,
      scrub: scrubSettings = DEFAULT_SCRUB_SETTINGS,
      shiftStep,
      step,
      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 scrub = useNumberScrub({
        disabled,
        format,
        formatValue,
        logo,
        max: normalizedMax,
        min: normalizedMin,
        onChange: setValue,
        onValueCommit,
        defaultResetValue: resetValue,
        scrub: scrubSettings,
        selectOnEdit: inputSettings.selectOnEdit,
        shiftStep,
        step: typeof step === "number" ? step : undefined,
        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>
      )
    }
    
    /* Required for Calligraph slot clipping in ScrubNumberInput.
       Import alongside scrub-number-input.tsx:
         import "./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;
    }
    
    /* Transitions.dev — bound-limit shake (adapted for scrub fields) */
    
    :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;
    }
    
    /* Boundary feedback: border pulse on blocked attempt */
    @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,
    [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;
      }
    }
    
    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
    }
    
    /** Minimal display by default; decimals only after the user typed them. */
    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)
    }
    
    /** Keeps the visible draft string when entering edit if it still matches the numeric value. */
    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
    }
    
    /** Keeps only characters valid while typing a numeric draft (+, -, digits, one dot). */
    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
    }
    
    import { useLayoutEffect, useState, type RefObject } from "react"
    
    /**
     * Detects when display content overflows its container (scrollWidth > clientWidth).
     * Used for Figma-style right-anchored number fields: leading digits are masked,
     * trailing digits stay visible, and consumers can expose the full value via title/aria.
     */
    export function measureDisplayOverflow(element: HTMLElement | null) {
      if (!element) {
        return false
      }
    
      return element.scrollWidth > element.clientWidth + 1
    }
    
    export 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
    }
    
    export const spring = {
      fast: {
        type: "spring" as const,
        duration: 0.08,
        bounce: 0,
        exit: { duration: 0.06 },
      },
      // Critically damped: same perceived speed as a bouncier tier, but lands
      // exactly with no overshoot — for short travel and panels/sheets that must
      // settle precisely (dropdowns, tabs, drawers, merged selection backgrounds).
      moderate: {
        type: "spring" as const,
        duration: 0.16,
        bounce: 0,
        exit: { duration: 0.12 },
      },
      slow: {
        type: "spring" as const,
        duration: 0.24,
        bounce: 0.12,
        exit: { duration: 0.16 },
      },
    } as const;
    
    // Fallback delay (ms) for deferred-unmount timers that guard an exit tween:
    // popups keep their portal mounted until onAnimationComplete fires, but a
    // throttled/background tab can stall the animation, so a timer force-unmounts
    // after the tier's exit duration plus a safety buffer. Deriving it here keeps
    // the timers in step with the tokens above.
    export const exitFallbackMs = (tier: { exit: { duration: number } }) =>
      Math.round(tier.exit.duration * 1000) + 100;
    
    import * as React from "react"
    
    // use-layout-effect.tsx
    // https://github.com/radix-ui/primitives/blob/main/packages/react/use-layout-effect/src/use-layout-effect.tsx
    
    /**
     * On the server, React emits a warning when calling `useLayoutEffect`.
     * This is because neither `useLayoutEffect` nor `useEffect` run on the server.
     * We use this safe version which suppresses the warning by replacing it with a noop on the server.
     *
     * See: https://reactjs.org/docs/hooks-reference.html#uselayouteffect
     */
    const useLayoutEffect = globalThis?.document ? React.useLayoutEffect : () => {}
    
    // use-controllable-state.tsx
    // https://github.com/radix-ui/primitives/blob/main/packages/react/use-controllable-state/src/use-controllable-state.tsx
    
    // Prevent bundlers from trying to optimize the import
    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
    
      // OK to disable conditionally calling hooks here because they will always run
      // consistently in the same environment. Bundlers should be able to remove the
      // code block entirely in production.
      /* 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"
    }
    
  4. Update import paths

    // Update @/ import paths in copied files to match your tsconfig paths aliases.