{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "unit-input",
  "title": "Unit Input",
  "description": "Ridiculously typed CSS-unit input with built-in deg/%/px/rem/em/vw/vh validators, pointer-locked drag scrubbing, and pluggable custom units.",
  "dependencies": [],
  "registryDependencies": [
    "input"
  ],
  "files": [
    {
      "path": "src/components/ui/unit-input/index.ts",
      "content": "export type { UnitInputProps } from \"./unit-input\"\nexport { UnitInput } from \"./unit-input\"\nexport type {\n  DegLiteral,\n  DegString,\n  EmLiteral,\n  EmString,\n  KnownUnit,\n  PercentLiteral,\n  PercentString,\n  PxLiteral,\n  PxString,\n  RemLiteral,\n  RemString,\n  UnitLiteral,\n  UnitString,\n  UnitStringMap,\n  VhLiteral,\n  VhString,\n  VwLiteral,\n  VwString,\n} from \"./unit-input.types\"\nexport {\n  deg,\n  em,\n  percent,\n  px,\n  rem,\n  vh,\n  vw,\n} from \"./unit-input.types\"\n",
      "type": "registry:ui",
      "target": "components/ui/unit-input/index.ts"
    },
    {
      "path": "src/components/ui/unit-input/unit-input.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Input } from \"@/components/ui/input\"\nimport { cn } from \"@/lib/utils\"\nimport type { KnownUnit, UnitStringMap } from \"./unit-input.types\"\n\nexport interface UnitInputProps<\n  TUnit extends KnownUnit | (string & {}) = KnownUnit | (string & {}),\n> {\n  value: TUnit extends KnownUnit ? UnitStringMap[TUnit] | (string & {}) : string\n  onChange: (\n    next: TUnit extends KnownUnit ? UnitStringMap[TUnit] : string,\n  ) => void\n  unit: TUnit\n  min?: number\n  max?: number\n  step?: number\n  precision?: number\n  dragSensitivity?: number\n  prefix?: React.ReactNode\n  suffix?: React.ReactNode\n  disabled?: boolean\n  \"aria-label\"?: string\n  className?: string\n}\n\nfunction parseNumericResult(\n  value: string,\n  unit: string,\n): { value: number; ok: boolean } {\n  // Reject a wrong suffix: parseFloat(\"45px\") would silently accept it for unit=\"deg\".\n  if (value !== \"\" && unit !== \"\" && !value.endsWith(unit)) {\n    return { value: 0, ok: false }\n  }\n  const stripped = value.endsWith(unit) ? value.slice(0, -unit.length) : value\n  const n = Number.parseFloat(stripped)\n  if (Number.isNaN(n)) return { value: 0, ok: false }\n  // Reject trailing garbage that parseFloat would otherwise tolerate.\n  if (!/^-?\\d+(\\.\\d+)?$/.test(stripped)) return { value: 0, ok: false }\n  return { value: n, ok: true }\n}\n\nfunction clamp(n: number, min: number | undefined, max: number | undefined) {\n  if (min !== undefined && n < min) return min\n  if (max !== undefined && n > max) return max\n  return n\n}\n\nexport function UnitInput<TUnit extends KnownUnit | (string & {})>({\n  value,\n  onChange,\n  unit,\n  min,\n  max,\n  step = 1,\n  precision = 0,\n  dragSensitivity = 1,\n  prefix,\n  suffix,\n  disabled,\n  className,\n  \"aria-label\": ariaLabel,\n}: UnitInputProps<TUnit>) {\n  const unitStr = String(unit)\n  const warnedRef = React.useRef(false)\n  const { value: parsedFromValue, ok: parseOk } = parseNumericResult(\n    String(value),\n    unitStr,\n  )\n  React.useEffect(() => {\n    if (!parseOk && !warnedRef.current) {\n      warnedRef.current = true\n      console.warn(\n        `[UnitInput] could not parse value \"${String(value)}\" for unit \"${unitStr}\". Falling back to 0.`,\n      )\n    }\n  }, [parseOk, value, unitStr])\n  const [rawDraft, setRawDraft] = React.useState<string | null>(null)\n  const displayed = rawDraft ?? parsedFromValue.toFixed(precision)\n\n  const commit = (raw: string) => {\n    const parsed = Number.parseFloat(raw)\n    const next = Number.isNaN(parsed)\n      ? parsedFromValue\n      : clamp(parsed, min, max)\n    const rounded = Number(next.toFixed(precision))\n    const formatted = `${rounded}${unitStr}`\n    setRawDraft(null)\n    if (formatted !== String(value)) {\n      onChange(formatted as Parameters<typeof onChange>[0])\n    }\n  }\n\n  const stepValue = (\n    direction: 1 | -1,\n    modifier: { shift: boolean; alt: boolean },\n  ) => {\n    const multiplier = modifier.shift ? 10 : modifier.alt ? 0.1 : 1\n    const delta = step * multiplier * direction\n    let base = parsedFromValue\n    if (rawDraft !== null) {\n      // Use the live draft as the step base, but only when it's a real number.\n      // A bare `|| parsedFromValue` would discard a legit \"0\" (0 is falsy).\n      const p = Number.parseFloat(rawDraft)\n      base = Number.isNaN(p) ? parsedFromValue : p\n    }\n    commit(String(base + delta))\n  }\n\n  const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n    if (disabled) return\n    if (e.key === \"Enter\") {\n      e.preventDefault()\n      commit(e.currentTarget.value)\n    } else if (e.key === \"Escape\") {\n      e.preventDefault()\n      setRawDraft(null)\n    } else if (e.key === \"ArrowUp\") {\n      e.preventDefault()\n      stepValue(1, { shift: e.shiftKey, alt: e.altKey })\n    } else if (e.key === \"ArrowDown\") {\n      e.preventDefault()\n      stepValue(-1, { shift: e.shiftKey, alt: e.altKey })\n    }\n  }\n\n  const scrubRef = React.useRef<{\n    active: boolean\n    anchor: number\n    deltaPx: number\n    lastShift: boolean\n    lastAlt: boolean\n    appliedShift: boolean\n    appliedAlt: boolean\n    hasCommitted: boolean\n  }>({\n    active: false,\n    anchor: 0,\n    deltaPx: 0,\n    lastShift: false,\n    lastAlt: false,\n    appliedShift: false,\n    appliedAlt: false,\n    hasCommitted: false,\n  })\n\n  const rafPendingRef = React.useRef(false)\n\n  // Hold the latest move-handler in a ref so the window listeners can register\n  // once (empty deps) instead of re-binding on every render.\n  const scrubMoveRef = React.useRef<(event: PointerEvent) => void>(() => {})\n  scrubMoveRef.current = (event: PointerEvent) => {\n    if (!scrubRef.current.active) return\n    scrubRef.current.deltaPx += event.movementX\n    scrubRef.current.lastShift = event.shiftKey\n    scrubRef.current.lastAlt = event.altKey\n    if (rafPendingRef.current) return\n    rafPendingRef.current = true\n    requestAnimationFrame(() => {\n      rafPendingRef.current = false\n      if (!scrubRef.current.active) return\n      const prevShift = scrubRef.current.appliedShift\n      const prevAlt = scrubRef.current.appliedAlt\n      const newShift = scrubRef.current.lastShift\n      const newAlt = scrubRef.current.lastAlt\n      if (\n        scrubRef.current.hasCommitted &&\n        (prevShift !== newShift || prevAlt !== newAlt)\n      ) {\n        // Fold accumulated delta into the anchor under the old multiplier, then\n        // zero it — so the new modifier never retroactively rescales past motion.\n        const prevMul = prevShift ? 10 : prevAlt ? 0.1 : 1\n        scrubRef.current.anchor =\n          scrubRef.current.anchor +\n          scrubRef.current.deltaPx * step * dragSensitivity * prevMul\n        scrubRef.current.deltaPx = 0\n      }\n      scrubRef.current.appliedShift = newShift\n      scrubRef.current.appliedAlt = newAlt\n      scrubRef.current.hasCommitted = true\n      const multiplier = newShift ? 10 : newAlt ? 0.1 : 1\n      const next =\n        scrubRef.current.anchor +\n        scrubRef.current.deltaPx * step * dragSensitivity * multiplier\n      commit(String(next))\n    })\n  }\n\n  React.useEffect(() => {\n    const onPointerMove = (event: PointerEvent) => scrubMoveRef.current(event)\n    const onPointerUp = () => {\n      if (!scrubRef.current.active) return\n      scrubRef.current.active = false\n      document.exitPointerLock()\n    }\n    window.addEventListener(\"pointermove\", onPointerMove)\n    window.addEventListener(\"pointerup\", onPointerUp)\n    return () => {\n      window.removeEventListener(\"pointermove\", onPointerMove)\n      window.removeEventListener(\"pointerup\", onPointerUp)\n    }\n  }, [])\n\n  const onSuffixPointerDown = (event: React.PointerEvent<HTMLElement>) => {\n    if (disabled) return\n    event.preventDefault()\n    scrubRef.current = {\n      active: true,\n      anchor: parsedFromValue,\n      deltaPx: 0,\n      lastShift: false,\n      lastAlt: false,\n      appliedShift: false,\n      appliedAlt: false,\n      hasCommitted: false,\n    }\n    event.currentTarget.requestPointerLock()\n  }\n\n  const suffixNode =\n    suffix === undefined ? (\n      <span\n        data-slot=\"unit-input-suffix\"\n        className={cn(\n          \"flex select-none items-center bg-muted/50 px-2 font-mono text-muted-foreground text-xs\",\n          disabled ? \"cursor-not-allowed\" : \"cursor-ew-resize\",\n        )}\n        aria-hidden=\"true\"\n        onPointerDown={onSuffixPointerDown}\n      >\n        {unitStr}\n      </span>\n    ) : (\n      <div data-slot=\"unit-input-suffix\" onPointerDown={onSuffixPointerDown}>\n        {suffix}\n      </div>\n    )\n\n  return (\n    <div\n      data-slot=\"unit-input\"\n      className={cn(\n        \"inline-flex h-7 items-stretch overflow-hidden rounded-md border border-input bg-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-1\",\n        disabled && \"opacity-50\",\n        className,\n      )}\n    >\n      {prefix ? (\n        <div\n          data-slot=\"unit-input-prefix\"\n          className=\"flex items-center border-input border-r bg-muted/50 px-2\"\n        >\n          {prefix}\n        </div>\n      ) : null}\n      <Input\n        value={displayed}\n        disabled={disabled}\n        aria-label={ariaLabel}\n        onChange={(e) => setRawDraft(e.target.value)}\n        onBlur={(e) => commit(e.target.value)}\n        onKeyDown={onKeyDown}\n        className=\"h-full rounded-none border-0 bg-transparent px-2 font-mono text-xs shadow-none focus-visible:ring-0 focus-visible:ring-offset-0\"\n      />\n      <div className=\"w-px bg-input\" aria-hidden=\"true\" />\n      {suffixNode}\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/unit-input/unit-input.tsx"
    },
    {
      "path": "src/components/ui/unit-input/unit-input.types.ts",
      "content": "// =====================================================================\n// 1. PRIMITIVES — private helpers, not exported\n// =====================================================================\n\ntype Digit = \"0\" | \"1\" | \"2\" | \"3\" | \"4\" | \"5\" | \"6\" | \"7\" | \"8\" | \"9\"\n\ntype WS = \" \" | \"\\n\" | \"\\t\"\ntype TrimLeft<S extends string> = S extends `${WS}${infer R}` ? TrimLeft<R> : S\ntype TrimRight<S extends string> = S extends `${infer R}${WS}`\n  ? TrimRight<R>\n  : S\ntype Trim<S extends string> = TrimLeft<TrimRight<S>>\n\ntype AllChars<S extends string, Allowed extends string> = S extends \"\"\n  ? true\n  : S extends `${infer C}${infer R}`\n    ? C extends Allowed\n      ? AllChars<R, Allowed>\n      : false\n    : false\n\ntype NonEmptyAllChars<S extends string, Allowed extends string> = S extends \"\"\n  ? false\n  : AllChars<S, Allowed>\n\ntype And<A extends boolean, B extends boolean> = A extends true\n  ? B extends true\n    ? true\n    : false\n  : false\n\ntype IsIntPart<S extends string> = S extends \"\"\n  ? true\n  : NonEmptyAllChars<S, Digit>\n\ntype IsNonNegativeNumber<S extends string> = S extends `${infer I}.${infer F}`\n  ? And<IsIntPart<I>, NonEmptyAllChars<F, Digit>> extends true\n    ? true\n    : false\n  : NonEmptyAllChars<S, Digit>\n\ntype IsSignedDecimal<S extends string> = S extends `-${infer R}`\n  ? IsNonNegativeNumber<R>\n  : IsNonNegativeNumber<S>\n\ntype KeepIf<B extends boolean, S extends string> = B extends true ? S : never\n\n// =====================================================================\n// 2. STRICT VALIDATORS — exported, generic. Used by deg()/percent()/etc.\n// =====================================================================\n\n// Validate that S is `<signed-decimal><U>` and, if so, return S unchanged\n// (else never). All per-unit *Literal aliases are just this with U pinned.\nexport type SuffixLiteral<\n  S extends string,\n  U extends string,\n> = S extends `${infer N}${U}` ? KeepIf<IsSignedDecimal<Trim<N>>, S> : never\n\nexport type DegLiteral<S extends string> = SuffixLiteral<S, \"deg\">\nexport type PercentLiteral<S extends string> = SuffixLiteral<S, \"%\">\nexport type PxLiteral<S extends string> = SuffixLiteral<S, \"px\">\nexport type RemLiteral<S extends string> = SuffixLiteral<S, \"rem\">\nexport type EmLiteral<S extends string> = SuffixLiteral<S, \"em\">\nexport type VwLiteral<S extends string> = SuffixLiteral<S, \"vw\">\nexport type VhLiteral<S extends string> = SuffixLiteral<S, \"vh\">\n\nexport type UnitLiteral<S extends string> =\n  | DegLiteral<S>\n  | PercentLiteral<S>\n  | PxLiteral<S>\n  | RemLiteral<S>\n  | EmLiteral<S>\n  | VwLiteral<S>\n  | VhLiteral<S>\n\n// =====================================================================\n// 3. SUGGESTION STRINGS — non-generic, for IntelliSense + onChange returns.\n// =====================================================================\n\nexport type SuffixString<U extends string> = `${number}${U}`\n\nexport type DegString = SuffixString<\"deg\">\nexport type PercentString = SuffixString<\"%\">\nexport type PxString = SuffixString<\"px\">\nexport type RemString = SuffixString<\"rem\">\nexport type EmString = SuffixString<\"em\">\nexport type VwString = SuffixString<\"vw\">\nexport type VhString = SuffixString<\"vh\">\n\nexport interface UnitStringMap {\n  deg: DegString\n  \"%\": PercentString\n  px: PxString\n  rem: RemString\n  em: EmString\n  vw: VwString\n  vh: VhString\n}\n\nexport type KnownUnit = keyof UnitStringMap\nexport type UnitString =\n  | DegString\n  | PercentString\n  | PxString\n  | RemString\n  | EmString\n  | VwString\n  | VhString\n\n// =====================================================================\n// 4. STRICT HELPERS — validate at the call site, return the literal back.\n// =====================================================================\n\n// Build a strict tag helper for one unit suffix: it accepts S only when S is a\n// valid `<number><U>` literal and returns S unchanged, so callers keep the\n// narrow type. Each per-unit helper below is one application of this factory.\nconst makeUnit =\n  <U extends string>() =>\n  <S extends string>(value: S & SuffixLiteral<S, U>): S =>\n    value\n\nexport const deg = makeUnit<\"deg\">()\nexport const percent = makeUnit<\"%\">()\nexport const px = makeUnit<\"px\">()\nexport const rem = makeUnit<\"rem\">()\nexport const em = makeUnit<\"em\">()\nexport const vw = makeUnit<\"vw\">()\nexport const vh = makeUnit<\"vh\">()\n",
      "type": "registry:ui",
      "target": "components/ui/unit-input/unit-input.types.ts"
    }
  ],
  "type": "registry:ui"
}