{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "number-field",
  "type": "registry:ui",
  "title": "Number field",
  "description": "A number you step, scrub or type: keycaps at each end of a pill well; each step turns the value one drum step (plus up, minus down) and an arrow past a limit shakes only the digits.",
  "dependencies": [
    "@base-ui/react@^1.8.0"
  ],
  "registryDependencies": [
    "https://metalui.dev/r/tokens.json"
  ],
  "files": [
    {
      "path": "packages/metalui/src/components/number-field/number-field.tsx",
      "type": "registry:ui",
      "target": "components/metalui/number-field/number-field.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\nimport { NumberField as BaseNumberField } from '@base-ui/react/number-field';\nimport { buttonClasses } from '../button/button';\nimport { SwapText } from '../../motion/swap';\nimport { refuse } from '../../motion/refuse';\n\n/* ─────────────────────────────────────────────────────────\n * NUMBER FIELD, a number you step, scrub or type, on Base UI NumberField\n *\n *   step      − and + are compact keycaps at the ends of a pill well; each press sinks the cap\n *             (the button press) and turns the value one drum step on the settle spring:\n *             + rolls up (the new number comes from below), − rolls down; holding repeats\n *   arrows    ↑ ↓ step the same way; Shift steps by 10\n *   scrub     drag the label sideways: each change turns the drum the way it went\n *   limit     at min or max that keycap disables; an arrow past it shakes only the digits\n *             on the refusal spring (a nest aside)\n *   type      plain text while typing (no drum); it commits and formats on blur\n *   invalid   the foundation's invalid ring on the group; aria-invalid on the input\n * Reduce Motion: the drum crossfades and nothing shakes.\n * The drum always holds the input's text; while it turns, the input's own text is hidden and the\n * drum shows over it.\n * Slots: NumberField.Root, NumberField.Label, NumberField.Group, NumberField.Decrement,\n * NumberField.Input, NumberField.Increment.\n * ───────────────────────────────────────────────────────── */\n\nconst ROOT = 'mu-number-field inline-grid gap-number-field-gap';\nconst LABEL = 'mu-number-field-label type-ui text-ink cursor-ew-resize select-none w-max';\nconst GROUP = 'mu-number-field-group group/nf inline-flex items-center h-number-field-height w-number-field-width p-number-field-pad box-border rounded-pill recipe-well-field focus-within:focus-ring-flush data-disabled:opacity-number-field-disabled relative data-invalid:invalid-ring';\nconst KEY = `${buttonClasses('standard', 'compact')} mu-number-field-key flex-none size-number-field-key-size px-0 justify-center type-ui group-data-disabled/nf:opacity-100!`;\nconst WINDOW = 'mu-number-field-window relative grid flex-1 min-w-0 h-full place-items-center overflow-hidden';\nconst INPUT = 'mu-number-field-input col-start-1 row-start-1 w-full min-w-0 h-full p-0 border-0 outline-none bg-transparent text-center type-lead tabular-nums text-ink caret-field-field-caret data-[turning]:text-transparent';\nconst DRUM = 'mu-number-field-drum col-start-1 row-start-1 pointer-events-none type-lead tabular-nums text-ink';\n\nconst STEPPED = new Set(['increment-press', 'decrement-press', 'keyboard', 'wheel', 'scrub']);\n\nexport interface NumberFieldProps extends Omit<BaseNumberField.Root.Props, 'className' | 'children'> {\n  /** The label above; drag it sideways to scrub. */\n  label?: React.ReactNode;\n  /** Accessible names for the keycaps. */\n  decrementLabel?: string;\n  incrementLabel?: string;\n  /** The value will not be accepted: the foundation's invalid ring, and aria-invalid on the input. */\n  invalid?: boolean;\n  className?: string;\n}\n\n/** A number you step, scrub or type. */\nfunction Root({ label, decrementLabel = 'Decrease', incrementLabel = 'Increase', invalid, className, onValueChange, value, defaultValue, min, max, ...props }: NumberFieldProps) {\n  const input = React.useRef<HTMLInputElement>(null);\n  const drum = React.useRef<HTMLSpanElement>(null);\n  const [current, setCurrent] = React.useState<number | null>(value ?? defaultValue ?? null);\n  const [face, setFace] = React.useState('');\n  const [turn, setTurn] = React.useState<{ n: number; dir: 'up' | 'down' } | null>(null);\n  const shown = value !== undefined ? value : current;\n  const labelId = React.useId();\n\n  // The drum shows exactly what the input shows (its formatted text), read after each render.\n  React.useLayoutEffect(() => { if (input.current) setFace(input.current.value); }, [shown]);\n\n  // The input's own text comes back once the drum has settled.\n  React.useEffect(() => {\n    if (!turn) return;\n    const ms = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--mu-spring-settle-d')) * 1000 || 440;\n    const t = window.setTimeout(() => setTurn(null), ms);\n    return () => clearTimeout(t);\n  }, [turn]);\n\n  return (\n    <BaseNumberField.Root\n      className={className ? `${ROOT} ${className}` : ROOT}\n      value={value}\n      defaultValue={defaultValue}\n      min={min}\n      max={max}\n      onValueChange={(next, details) => {\n        const prev = shown;\n        if (value === undefined) setCurrent(next);\n        if (STEPPED.has(details.reason) && prev != null && next != null && next !== prev) {\n          setTurn((t) => ({ n: (t?.n ?? 0) + 1, dir: next > prev ? 'up' : 'down' }));\n        }\n        onValueChange?.(next, details);\n      }}\n      {...props}\n    >\n      {label != null && (\n        <BaseNumberField.ScrubArea className=\"w-max\">\n          <span id={labelId} className={LABEL}>{label}</span>\n        </BaseNumberField.ScrubArea>\n      )}\n      <BaseNumberField.Group className={GROUP} data-invalid={invalid ? '' : undefined}>\n        <BaseNumberField.Decrement className={KEY} aria-label={decrementLabel}>−</BaseNumberField.Decrement>\n        <span className={turn?.dir === 'down' ? `${WINDOW} number-field-down` : WINDOW} ref={drum}>\n          <BaseNumberField.Input\n            ref={input}\n            className={INPUT}\n            data-turning={turn ? '' : undefined}\n            aria-labelledby={label != null ? labelId : undefined}\n            aria-invalid={invalid || undefined}\n            onKeyDown={(e) => {\n              const up = e.key === 'ArrowUp', down = e.key === 'ArrowDown';\n              if ((up && max != null && shown != null && shown >= max) || (down && min != null && shown != null && shown <= min)) refuse(drum.current);\n            }}\n          />\n          <span aria-hidden className={turn ? DRUM : `${DRUM} invisible`}><SwapText value={face} /></span>\n        </span>\n        <BaseNumberField.Increment className={KEY} aria-label={incrementLabel}>+</BaseNumberField.Increment>\n      </BaseNumberField.Group>\n    </BaseNumberField.Root>\n  );\n}\n\nexport const NumberField = Object.assign(Root, {\n  Label: BaseNumberField.ScrubArea,\n  Group: BaseNumberField.Group,\n  Decrement: BaseNumberField.Decrement,\n  Input: BaseNumberField.Input,\n  Increment: BaseNumberField.Increment,\n  Root,\n});\n"
    }
  ],
  "docs": "Agent guide: https://metalui.dev/r/number-field.md. SwiftUI: MetalNumberField in the MetalUI Swift package."
}
