{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "unit-input",
  "title": "Unit Input",
  "description": "A number box with a unit beside it that settles what happens to the number when the unit changes. Reach for it wherever a form asks for a length rather than a count: a spacing, padding or radius field in a theme or design-token editor, a font-size control in a settings panel, page margins for a print or PDF export, a width or breakpoint in a layout builder, a canvas or image size, a CSS value in an admin or CMS style panel, a chart axis or gutter, a wrapper for the one CSS length a white-label tenant is allowed to set. Common asks it answers: \"unit input react\", \"shadcn unit input\", \"number input with unit dropdown\", \"css length input react\", \"px rem input react\", \"px to rem converter component\", \"input with unit selector shadcn\", \"length field with units react\", \"css unit picker react\", \"convert px to rem in javascript\", \"CSS.px(16).to('rem') throws\", \"Cannot convert to px typed om\", \"CSSUnitValue to rem TypeError\", \"how many px is 1rem javascript\", \"get root font size javascript\", \"getComputedStyle fontSize rem base\", \"px rem round trip loses value\", \"1.06rem is not 17px\", \"value changes when I switch units\", \"unit dropdown changes my number\", \"width 16 not working css\", \"unitless length invalid css\", \"css unitless zero only\", \"input type number step per unit\", \"arrow key step 0.25 rem\", \"0.30000000000000004 spacing input\", \"parse 1.5rem in javascript\", \"split number and unit javascript\", \"paste 1.5rem into input\", \"単位付き 数値入力 react\", \"px rem 切り替え 入力欄\", \"css 長さ 入力 コンポーネント\", \"単位を変えたら値が変わる\", \"design token editor length input\", \"tailwind spacing scale input\", \"css custom property length input\", \"css variable px rem editor\", \"style dictionary token input react\", \"token studio px rem\", \"storybook control for css length\", \"theme editor spacing field react\", \"border radius input react\", \"letter spacing input react\", \"clamp value input react\", \"viewport unit input vw vh react\", \"print margin input mm inch\", \"デザイントークン 長さ 入力\", \"余白設定 px rem 切り替え\". **Changing the unit is the whole component, because there are two different right answers and picking silently is what makes a field untrustworthy.** Converting keeps the rendered length and moves the number (16px becomes 1rem); reinterpreting keeps the number and moves the length (16px becomes 16rem, which renders at 256px — sixteen times bigger). Both are what somebody means, in different moments, so `behavior` names the one you want and the default converts. **It will not convert a unit it cannot account for, and that refusal is the platform's own.** Typed OM converts any fixed unit into any other — measured in Chrome 152, `CSS.px(96).to(\"in\")` is `1in` and `.to(\"cm\")` is `2.54cm` — and throws `TypeError: Cannot convert to px` on rem, em, %, ch, ex, lh, vw, vh, vmin and vmax, all ten of them, because a number does not know how big it is. So the fixed units convert with no help, and the relative ones convert only once `base` says what they are worth; without it the number is carried across unchanged and the field says it was carried, rather than dividing by an assumed 16 and moving a value nobody touched. The base is the caller's because it is a fact about the page, and because it is live: setting the root font-size to 20px makes `width: 1rem` compute to 20px on the next read, so a component that cached 16 would be wrong from then on. `readBase(element)` is exported to read the current ones off the document in a line. **A round trip gives back what was typed, which rounding harder does not achieve.** Over 1 to 200px against a 16px root, rounding a px→rem→px trip to 2 decimals returns the original for 50 of the 200 — 17px leaves as 1.06rem and comes back 16.96px — and against a 15px root, somebody who set `html { font-size: 15px }`, exactly 65 of 200 survive at 2, 3 and 4 decimals alike, because 1/15 does not terminate in decimal and more places never recover it. The number is therefore remembered per unit for as long as the edit lasts and restored on the way back, not recomputed; typing is a new intention and drops the memory. **A bare number is refused except as zero, which is what CSS does.** Measured, `CSS.supports(\"width\", \"16\")` is false while `\"0\"`, `\"0.0\"` and `\"-0\"` are all true, and assigning the unitless string to `element.style.width` is a silent no-op that leaves the previous value standing — so a non-zero number with no unit is marked invalid and never posted, while `\"\"` can still be offered in the list for the properties where it is legal (`line-height: 1.5` and `z-index: 3` both measured valid). **The arrow keys step by the unit on screen**, 1 for px and 0.25 for rem and em out of `DEFAULT_STEPS`, with Shift taking ten at once, and each step rounds to the decimals in play because ten additions of 0.1 in IEEE 754 land on 0.9999999999999999. `minPx`/`maxPx` are given in pixels so one bound means one thing in every unit; they are reported rather than silently applied while typing, and the arrow keys stop at them. Pasting `1.5rem` into the number box splits it and moves the unit, since that is how a value copied out of devtools arrives; unit identifiers are matched case-insensitively (`16PX`, `1q`) and the canonical spelling comes back. Parsing is stricter than `Number` on the reasoning this registry's number-range-input gives: `1e3`, `0x10`, `Infinity` and `1,5` are all numbers to `Number` and none of them is a length somebody typed, so they are refused and the text stays on screen to be corrected instead of being deleted out from under the reader. The two controls are one `role=\"group\"` with a label, each separately labelled, the box is `type=\"text\"` with `inputMode=\"decimal\"` rather than `type=\"number\"` — a number input discards text it cannot parse, so `16q` would vanish instead of being explained — one polite status line carries either the resolved pixel length or the problem and is wired to the box with `aria-describedby`, `aria-invalid` follows the actual fault, and a `name` posts canonical CSS (`1.5rem`) through a hidden input and posts nothing when the value is one it would not stand behind. `parseUnitValue`, `convertLength`, `resolvePx`, `toCssLength`, `stepValue` and `ABSOLUTE_PX` are exported as plain functions so the route handler that receives the string reaches the same answer the field showed. The fixed table is derived from cm the way CSS defines it rather than written out, which is also the only form that agrees with the engine bit for bit: `96/25.4` is 3.7795275590551185 in IEEE 754 where Chrome reports 3.7795275590551176. Official shadcn/ui has nothing of the kind and the measurement is not close: fetching every entry in its registry today — 65 listed, 62 fetchable, `code-block`, `questionnaire` and `trace` 404 on both style tracks — and searching all 223,287 bytes of it, `convertUnit`, `parseUnit`, `unitless`, `CSSUnitValue`, `getComputedStyle`, `fontSize`, `toFixed`, `\"rem\"`, `'rem'` and `unitSystem` are every one of them a zero hit; its `input` is a bare text box and `input-group` only puts ornaments either side of one, and official never reads a computed style anywhere. Within pulld it is the length end of the field family: `number-input` counts things with steppers, `number-range-input` holds a from-and-to pair, `currency-input` formats money and `duration-input` reads a span of time — each of those is one unit that never changes, and this is the one where the unit is part of the value. No dependencies beyond React and your `cn`.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/ui/unit-input.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/**\n * How many pixels one of each fixed-size unit is.\n *\n * Measured rather than remembered. The platform already ships this table: in Chrome 152,\n * `CSS.px(96).to(\"in\")` returns `1in` and `.to(\"cm\")` returns `2.54cm`, and `CSS.pc(1).to(\"px\")`\n * returns 16. The ratios below are those same conversions written out, so this file agrees with\n * the engine instead of with somebody's memory of the CSS spec.\n */\nexport const ABSOLUTE_PX = {\n  px: 1,\n  in: 96,\n  pc: 96 / 6,\n  pt: 96 / 72,\n  cm: 96 / 2.54,\n  // Derived from cm rather than written as 96/25.4 and 96/101.6, which is how CSS defines them\n  // (1mm is a tenth of a cm, 1Q a fortieth) and also the only form that agrees with the engine:\n  // 96/25.4 is 3.7795275590551185 in IEEE 754 while Chrome reports 3.7795275590551176, and the\n  // difference is the last two bits of the division order.\n  mm: 96 / 2.54 / 10,\n  Q: 96 / 2.54 / 40,\n} as const\n\nexport type AbsoluteUnit = keyof typeof ABSOLUTE_PX\n\n/**\n * The units whose size is decided somewhere outside the value.\n *\n * This list is not a stylistic grouping, it is where the platform draws the line: every one of\n * these throws on conversion. Measured in Chrome 152, `CSS.rem(1).to(\"px\")` is a\n * `TypeError: Cannot convert to px`, and so is the same call on `em`, `%`, `ch`, `ex`, `lh`, `vw`,\n * `vh`, `vmin` and `vmax` — ten units, one for one with this array. Typed OM will convert any\n * absolute unit into any other absolute unit and refuses all of these, because the number alone\n * does not know how big it is. Neither does this component, which is why `base` exists.\n */\nexport const RELATIVE_UNITS = [\n  \"rem\",\n  \"em\",\n  \"%\",\n  \"ch\",\n  \"ex\",\n  \"lh\",\n  \"vw\",\n  \"vh\",\n  \"vmin\",\n  \"vmax\",\n] as const\n\nexport type RelativeUnit = (typeof RELATIVE_UNITS)[number]\n\n/** `\"\"` is \"no unit\", which CSS accepts for a length only when the number is zero. */\nexport type Unit = AbsoluteUnit | RelativeUnit | \"\"\n\n/** Pixels per one unit, for whichever relative units the document can account for. */\nexport type UnitBase = Partial<Record<RelativeUnit, number>>\n\nexport const ALL_UNITS: readonly Unit[] = [\n  ...(Object.keys(ABSOLUTE_PX) as AbsoluteUnit[]),\n  ...RELATIVE_UNITS,\n]\n\nexport interface UnitValue {\n  value: number\n  unit: Unit\n}\n\nexport function isAbsoluteUnit(unit: Unit): unit is AbsoluteUnit {\n  return unit !== \"\" && Object.prototype.hasOwnProperty.call(ABSOLUTE_PX, unit)\n}\n\n/** Pixels per one `unit`, or `null` when nothing in `base` says how big it is. */\nexport function pxPerUnit(unit: Unit, base: UnitBase = {}): number | null {\n  if (unit === \"\") return 1\n  if (isAbsoluteUnit(unit)) return ABSOLUTE_PX[unit]\n  const supplied = base[unit]\n  return typeof supplied === \"number\" && Number.isFinite(supplied) ? supplied : null\n}\n\n/**\n * The length in pixels, or `null` when it cannot be known here.\n *\n * A bare number is only a length when it is zero, so everything else unitless resolves to `null`\n * rather than being quietly treated as pixels — measured, `CSS.supports(\"width\", \"16\")` is false\n * while `CSS.supports(\"width\", \"0\")` is true.\n */\nexport function resolvePx(\n  value: number,\n  unit: Unit,\n  base: UnitBase = {}\n): number | null {\n  if (!Number.isFinite(value)) return null\n  if (unit === \"\") return value === 0 ? 0 : null\n  const per = pxPerUnit(unit, base)\n  return per === null ? null : value * per\n}\n\n/**\n * The same length counted in `to`, or `null` when that would require a guess.\n *\n * Returning `null` is the whole point: the alternative is inventing a base, and an invented base\n * moves the value by exactly the factor it got wrong.\n */\nexport function convertLength(\n  value: number,\n  from: Unit,\n  to: Unit,\n  base: UnitBase = {}\n): number | null {\n  if (from === to) return value\n  const px = resolvePx(value, from, base)\n  if (px === null) return null\n  if (to === \"\") return px === 0 ? 0 : null\n  const per = pxPerUnit(to, base)\n  return per === null || per === 0 ? null : px / per\n}\n\n/**\n * A number, then optionally a unit: `16`, `16px`, `16 PX`, `1.5rem`, `.5em`, `50%`, `-2px`.\n *\n * Deliberately stricter than `Number`, on the same reasoning this registry's number-range-input\n * gives: `Number(\"1e3\")` is 1000 and `Number(\"0x10\")` is 16, and neither is something a person\n * typed into a length field, so both are refused here instead of becoming a value nobody entered.\n */\nexport function parseUnitValue(\n  text: string,\n  units: readonly Unit[] = ALL_UNITS\n): UnitValue | null {\n  const trimmed = text.trim()\n  if (trimmed === \"\") return null\n  const parts = /^([+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+))\\s*([a-zA-Z%]*)$/.exec(trimmed)\n  if (!parts) return null\n  const value = Number(parts[1])\n  if (!Number.isFinite(value)) return null\n  const written = parts[2]\n  if (written === \"\") return { value, unit: \"\" }\n  // CSS unit identifiers are case-insensitive, so `16PX` is a real thing to paste in; the canonical\n  // spelling from `units` is what comes back out.\n  const matched = units.find(\n    (unit) => unit !== \"\" && unit.toLowerCase() === written.toLowerCase()\n  )\n  return matched === undefined ? null : { value, unit: matched }\n}\n\n/** Counts a number's decimals, so repeated nudges do not drift. */\nfunction decimalPlaces(n: number): number {\n  const s = String(n)\n  const dot = s.indexOf(\".\")\n  if (dot === -1) return 0\n  if (s.includes(\"e\") || s.includes(\"E\")) return 0\n  return s.length - dot - 1\n}\n\n/** `1.0600` -> `\"1.06\"`, `2.000` -> `\"2\"`, `100` -> `\"100\"`. */\nexport function formatNumber(value: number, decimals: number): string {\n  if (!Number.isFinite(value)) return \"\"\n  const fixed = value.toFixed(Math.max(0, Math.min(decimals, 20)))\n  // Only a string that has a decimal point may lose trailing zeros — stripping them unconditionally\n  // turns 100 into 1.\n  return fixed.includes(\".\") ? fixed.replace(/\\.?0+$/, \"\") : fixed\n}\n\n/** The canonical CSS the field stands for: `1.5rem`, `0`, `50%`. */\nexport function toCssLength(value: number, unit: Unit, decimals = 4): string {\n  return `${formatNumber(value, decimals)}${unit}`\n}\n\n/**\n * A step per unit, because one step for all of them is unusable.\n *\n * A pixel steps by 1 and a rem by 1 would be a sixteen-pixel jump; a rem steps by 0.25 and a pixel\n * by 0.25 would take four presses to move a pixel.\n */\nexport const DEFAULT_STEPS: Readonly<Record<Unit, number>> = {\n  \"\": 1,\n  px: 1,\n  in: 0.125,\n  pc: 0.5,\n  pt: 1,\n  cm: 0.1,\n  mm: 1,\n  Q: 1,\n  rem: 0.25,\n  em: 0.25,\n  \"%\": 1,\n  ch: 0.5,\n  ex: 0.5,\n  lh: 0.25,\n  vw: 1,\n  vh: 1,\n  vmin: 1,\n  vmax: 1,\n}\n\nexport function stepFor(\n  unit: Unit,\n  steps: Partial<Record<Unit, number>> = {}\n): number {\n  const given = steps[unit]\n  if (typeof given === \"number\" && Number.isFinite(given) && given > 0) return given\n  return DEFAULT_STEPS[unit] ?? 1\n}\n\n/**\n * Adds one step and rounds to the decimals actually in play.\n *\n * Without the rounding a 0.1 step walks off: measured, ten additions of 0.1 in IEEE 754 land on\n * 0.9999999999999999, which is what puts `0.30000000000000004` in a spacing field.\n */\nexport function stepValue(value: number, step: number, direction: 1 | -1): number {\n  const next = value + direction * step\n  const places = Math.min(Math.max(decimalPlaces(step), decimalPlaces(value)), 10)\n  return Number(next.toFixed(places))\n}\n\n/**\n * Reads the bases the document is currently applying to `element`.\n *\n * Kept out of the component on purpose. The component never touches the DOM, both because the base\n * is genuinely the caller's to supply — it is a fact about the page, not about the field — and\n * because it is live: measured, setting the root font-size to 20px makes `width: 1rem` compute to\n * 20px on the very next read. A component that cached 16 would be wrong from then on.\n */\nexport function readBase(element: Element): UnitBase {\n  const own = getComputedStyle(element)\n  const root = getComputedStyle(element.ownerDocument.documentElement)\n  const parent = element.parentElement ?? element\n  const base: UnitBase = {}\n  const rem = parseFloat(root.fontSize)\n  if (Number.isFinite(rem)) base.rem = rem\n  const em = parseFloat(getComputedStyle(parent).fontSize)\n  if (Number.isFinite(em)) base.em = em\n  const lh = parseFloat(own.lineHeight)\n  if (Number.isFinite(lh)) base.lh = lh\n  const width = parseFloat(getComputedStyle(parent).width)\n  if (Number.isFinite(width)) base[\"%\"] = width / 100\n  const view = element.ownerDocument.defaultView\n  if (view) {\n    base.vw = view.innerWidth / 100\n    base.vh = view.innerHeight / 100\n    base.vmin = Math.min(view.innerWidth, view.innerHeight) / 100\n    base.vmax = Math.max(view.innerWidth, view.innerHeight) / 100\n  }\n  return base\n}\n\nexport interface UnitInputMessages {\n  resolved: (shown: string) => string\n  needsBase: (unit: Unit) => string\n  unitlessNonZero: () => string\n  notANumber: () => string\n  belowMin: (shown: string) => string\n  aboveMax: (shown: string) => string\n  reinterpreted: (unit: Unit) => string\n}\n\nconst DEFAULT_MESSAGES: UnitInputMessages = {\n  resolved: (shown) => `= ${shown}`,\n  needsBase: (unit) =>\n    `${unit} is sized by the document, so its length in pixels is not known here.`,\n  unitlessNonZero: () => \"A number on its own is only a length when it is 0 — pick a unit.\",\n  notANumber: () => \"Enter a number, optionally followed by a unit.\",\n  belowMin: (shown) => `Minimum ${shown}.`,\n  aboveMax: (shown) => `Maximum ${shown}.`,\n  reinterpreted: (unit) => `Read as ${unit}; the number was kept as typed.`,\n}\n\nexport interface UnitInputProps {\n  /** Controlled value. Leave undefined and use `defaultValue` for an uncontrolled field. */\n  value?: UnitValue\n  defaultValue?: UnitValue\n  /** Fires on every edit. `px` is the resolved length, or null when no base accounts for the unit. */\n  onValueChange?: (next: UnitValue & { px: number | null }) => void\n  /** Which units the select offers, in order. Include `\"\"` to offer \"no unit\". */\n  units?: readonly Unit[]\n  /** Pixels per one relative unit. Without it, rem/em/% are carried, not converted. */\n  base?: UnitBase\n  /**\n   * What changing the unit does to the number. `\"convert\"` keeps the rendered length and changes\n   * the number; `\"reinterpret\"` keeps the number and changes the length.\n   */\n  behavior?: \"convert\" | \"reinterpret\"\n  /** Per-unit arrow-key step, over `DEFAULT_STEPS`. */\n  steps?: Partial<Record<Unit, number>>\n  /** Decimals a converted number is displayed to (default 4). */\n  decimals?: number\n  /** Bounds in pixels, so one bound means the same thing in every unit. Reported, never silently applied. */\n  minPx?: number\n  maxPx?: number\n  /** Posts the canonical CSS length (`1.5rem`) under this name. */\n  name?: string\n  label?: string\n  unitLabel?: string\n  /** Shows the resolved pixel length under the field (default true). */\n  showResolved?: boolean\n  messages?: Partial<UnitInputMessages>\n  disabled?: boolean\n  required?: boolean\n  className?: string\n  id?: string\n}\n\nconst DEFAULT_UNITS: readonly Unit[] = [\"px\", \"rem\", \"em\", \"%\"]\n\nexport function UnitInput({\n  value,\n  defaultValue,\n  onValueChange,\n  units = DEFAULT_UNITS,\n  base,\n  behavior = \"convert\",\n  steps,\n  decimals = 4,\n  minPx,\n  maxPx,\n  name,\n  label = \"Length\",\n  unitLabel = \"Unit\",\n  showResolved = true,\n  messages,\n  disabled,\n  required,\n  className,\n  id,\n}: UnitInputProps) {\n  const say = React.useMemo(\n    () => ({ ...DEFAULT_MESSAGES, ...messages }),\n    [messages]\n  )\n  const bases = React.useMemo<UnitBase>(() => base ?? {}, [base])\n\n  // useId, not a module counter: a counter started on the server and a counter started in the\n  // browser do not agree, and the id is written into aria-describedby on both sides.\n  const generatedId = React.useId()\n  const fieldId = id ?? generatedId\n  const statusId = `${fieldId}-status`\n\n  const isControlled = value !== undefined\n  const initial = value ?? defaultValue ?? null\n\n  const [text, setText] = React.useState(() =>\n    initial ? formatNumber(initial.value, decimals) : \"\"\n  )\n  const [unit, setUnit] = React.useState<Unit>(\n    () => initial?.unit ?? units[0] ?? \"px\"\n  )\n  /** Says the last unit change carried the number instead of converting it. */\n  const [carried, setCarried] = React.useState<Unit | null>(null)\n\n  /**\n   * The exact number this field last held in each unit.\n   *\n   * This is what makes a round trip lossless, and rounding harder is not an alternative to it.\n   * Measured over 1..200px: rounding a px->rem->px trip to 2 decimals returns the original for only\n   * 50 of the 200, and with a 15px root — somebody who set `html { font-size: 15px }` — exactly 65\n   * of 200 survive at 2, 3 *and* 4 decimals, because 1/15 does not terminate in decimal and more\n   * places never recover it. So the number is not recomputed on the way back: it is remembered.\n   * Typing replaces the intent, so the memory is dropped then.\n   */\n  const memory = React.useRef<Map<Unit, number>>(new Map())\n\n  React.useEffect(() => {\n    if (!isControlled || !value) return\n    setText(formatNumber(value.value, decimals))\n    setUnit(value.unit)\n  }, [isControlled, value, decimals])\n\n  const parsed = React.useMemo(() => {\n    if (text.trim() === \"\") return null\n    return parseUnitValue(text, units.length ? units : ALL_UNITS)\n  }, [text, units])\n\n  // A unit written into the box wins over the select, which is how a value pasted out of devtools\n  // (\"1.5rem\") lands in one action instead of being typed and then chosen.\n  const effectiveUnit: Unit = parsed && parsed.unit !== \"\" ? parsed.unit : unit\n  const number = parsed ? parsed.value : null\n  const px = number === null ? null : resolvePx(number, effectiveUnit, bases)\n\n  const malformed = text.trim() !== \"\" && parsed === null\n  const unitlessNonZero =\n    number !== null && effectiveUnit === \"\" && number !== 0\n  const belowMin =\n    px !== null && typeof minPx === \"number\" && px < minPx\n  const aboveMax =\n    px !== null && typeof maxPx === \"number\" && px > maxPx\n  // An untouched empty field is not wrong yet, so `required` is left to the native attribute rather\n  // than marking a field the reader has not reached as invalid.\n  const invalid = malformed || unitlessNonZero || belowMin || aboveMax\n\n  const emit = React.useCallback(\n    (nextValue: number | null, nextUnit: Unit) => {\n      if (nextValue === null) return\n      onValueChange?.({\n        value: nextValue,\n        unit: nextUnit,\n        px: resolvePx(nextValue, nextUnit, bases),\n      })\n    },\n    [onValueChange, bases]\n  )\n\n  function commitText(next: string) {\n    setText(next)\n    setCarried(null)\n    memory.current.clear()\n    const read = parseUnitValue(next, units.length ? units : ALL_UNITS)\n    if (read === null) return\n    const nextUnit = read.unit === \"\" ? unit : read.unit\n    if (read.unit !== \"\" && read.unit !== unit) setUnit(read.unit)\n    memory.current.set(nextUnit, read.value)\n    emit(read.value, nextUnit)\n  }\n\n  function changeUnit(next: Unit) {\n    const from = effectiveUnit\n    setUnit(next)\n    if (number === null || next === from) {\n      setCarried(null)\n      return\n    }\n    memory.current.set(from, number)\n\n    if (behavior === \"reinterpret\") {\n      setText(formatNumber(number, decimals))\n      setCarried(null)\n      emit(number, next)\n      return\n    }\n\n    const remembered = memory.current.get(next)\n    if (remembered !== undefined) {\n      setText(formatNumber(remembered, decimals))\n      setCarried(null)\n      emit(remembered, next)\n      return\n    }\n\n    const converted = convertLength(number, from, next, bases)\n    if (converted === null) {\n      // Nothing here knows how big the new unit is, so the number is carried and said to be\n      // carried. Converting through an assumed base is the one thing that must not happen: it is\n      // how a field changes a value nobody asked it to change.\n      setText(formatNumber(number, decimals))\n      setCarried(next)\n      emit(number, next)\n      return\n    }\n    setText(formatNumber(converted, decimals))\n    setCarried(null)\n    memory.current.set(next, converted)\n    emit(converted, next)\n  }\n\n  function nudge(direction: 1 | -1, multiplier: number) {\n    if (disabled) return\n    const step = stepFor(effectiveUnit, steps) * multiplier\n    const from = number ?? 0\n    let next = stepValue(from, step, direction)\n    // A bound given in pixels is checked in pixels and then brought back, so \"at least 1rem\" holds\n    // whichever unit the field is showing.\n    const nextPx = resolvePx(next, effectiveUnit, bases)\n    if (nextPx !== null) {\n      if (typeof minPx === \"number\" && nextPx < minPx) {\n        const floor = convertLength(minPx, \"px\", effectiveUnit, bases)\n        if (floor !== null) next = Number(floor.toFixed(10))\n      }\n      if (typeof maxPx === \"number\" && nextPx > maxPx) {\n        const ceil = convertLength(maxPx, \"px\", effectiveUnit, bases)\n        if (ceil !== null) next = Number(ceil.toFixed(10))\n      }\n    }\n    setText(formatNumber(next, decimals))\n    setCarried(null)\n    memory.current.clear()\n    memory.current.set(effectiveUnit, next)\n    emit(next, effectiveUnit)\n  }\n\n  function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {\n    if (event.key !== \"ArrowUp\" && event.key !== \"ArrowDown\") return\n    event.preventDefault()\n    nudge(event.key === \"ArrowUp\" ? 1 : -1, event.shiftKey ? 10 : 1)\n  }\n\n  let status = \"\"\n  if (malformed) status = say.notANumber()\n  else if (unitlessNonZero) status = say.unitlessNonZero()\n  else if (belowMin && typeof minPx === \"number\")\n    status = say.belowMin(`${formatNumber(minPx, decimals)}px`)\n  else if (aboveMax && typeof maxPx === \"number\")\n    status = say.aboveMax(`${formatNumber(maxPx, decimals)}px`)\n  else if (carried !== null) status = say.reinterpreted(carried)\n  else if (showResolved && px !== null && effectiveUnit !== \"px\")\n    status = say.resolved(`${formatNumber(px, decimals)}px`)\n  else if (showResolved && px === null && number !== null && effectiveUnit !== \"\")\n    status = say.needsBase(effectiveUnit)\n\n  const offered = units.length ? units : DEFAULT_UNITS\n  const posted =\n    number !== null && !invalid ? toCssLength(number, effectiveUnit, decimals) : \"\"\n\n  return (\n    <div className={cn(\"inline-flex flex-col gap-1\", className)}>\n      <div\n        role=\"group\"\n        aria-label={label}\n        className={cn(\n          \"inline-flex h-9 items-center rounded-md border border-input bg-transparent shadow-sm transition-colors focus-within:ring-1 focus-within:ring-ring\",\n          invalid && \"border-destructive focus-within:ring-destructive\",\n          disabled && \"cursor-not-allowed opacity-50\"\n        )}\n      >\n        <input\n          id={fieldId}\n          type=\"text\"\n          inputMode=\"decimal\"\n          autoComplete=\"off\"\n          spellCheck={false}\n          value={text}\n          disabled={disabled}\n          required={required}\n          aria-invalid={invalid || undefined}\n          aria-describedby={status ? statusId : undefined}\n          onChange={(event) => commitText(event.target.value)}\n          onKeyDown={handleKeyDown}\n          className=\"h-full w-16 min-w-0 rounded-l-md bg-transparent px-2 text-sm tabular-nums outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed\"\n        />\n        <select\n          aria-label={unitLabel}\n          value={offered.includes(effectiveUnit) ? effectiveUnit : offered[0]}\n          disabled={disabled}\n          onChange={(event) => changeUnit(event.target.value as Unit)}\n          className=\"h-full cursor-pointer rounded-r-md border-l border-input bg-transparent py-0 pl-2 pr-1 text-sm text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed\"\n        >\n          {offered.map((option) => (\n            <option key={option || \"none\"} value={option}>\n              {option === \"\" ? \"—\" : option}\n            </option>\n          ))}\n        </select>\n        {name ? <input type=\"hidden\" name={name} value={posted} /> : null}\n      </div>\n      {status ? (\n        <span\n          id={statusId}\n          aria-live=\"polite\"\n          className={cn(\n            \"text-xs tabular-nums\",\n            invalid ? \"text-destructive\" : \"text-muted-foreground\"\n          )}\n        >\n          {status}\n        </span>\n      ) : null}\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui",
  "docs": "Install any pulld component by name: add \"@pulld\": \"https://pulld.pages.dev/r/{name}.json\" to the registries block in components.json, then `npx shadcn add @pulld/<name>`. All 140 components: https://pulld.pages.dev/?utm_source=cli"
}
