{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "input-digit",
  "title": "Input Digit",
  "description": "Input Digit from the Get IT Done design system.",
  "dependencies": [
    "class-variance-authority@^0.7.0"
  ],
  "registryDependencies": [
    "https://zero-design.itdone.de/r/labels-context.json",
    "https://zero-design.itdone.de/r/recipes.json",
    "https://zero-design.itdone.de/r/utils.json",
    "https://zero-design.itdone.de/r/theme.json"
  ],
  "files": [
    {
      "path": "registry/ui/input-digit.tsx",
      "content": "'use client';\n\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport * as React from 'react';\n\nimport { focusRing, invalidRing, transitionBase } from '@/lib/recipes';\nimport { cn } from '@/lib/utils';\nimport { useLabels, usePreset } from '@/components/ui/labels-context';\n\n// Box sizes follow the Figma InputDigit frames and are not bound to the control scale on purpose (owner decision):\n// sm 64px and md 80px carry the 48px digit role, lg 96px carries the 60px digit role.\nconst digitVariants = cva(\n  [\n    'rounded-control border border-solid border-input bg-background p-0 text-center font-medium tabular-nums text-foreground caret-transparent',\n    'placeholder:text-text-subtle shadow-elevation-control',\n    transitionBase,\n    'hover:border-border-bold',\n    focusRing,\n    invalidRing,\n    'disabled:cursor-not-allowed disabled:opacity-disabled',\n    'data-[success=true]:border-border-success',\n  ],\n  {\n    variants: {\n      size: {\n        sm: 'h-16 w-16 typography-digit-md',\n        md: 'h-20 w-20 typography-digit-md',\n        lg: 'h-24 w-24 typography-digit-lg',\n        // Verification code box (Figma _Code digit): 40 x 48, 16/600. Default size in the zero preset.\n        code: 'h-12 w-10 typography-kpi-value',\n      },\n    },\n    defaultVariants: { size: 'lg' },\n  },\n);\n\nexport interface InputDigitProps\n  extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange' | 'defaultValue'>,\n    VariantProps<typeof digitVariants> {\n  // Number of boxes. Figma ships 6 (default) and 4.\n  length?: number;\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  // Fires once when every box is filled.\n  onComplete?: (value: string) => void;\n  // `numeric` accepts 0-9 and opens the numeric keypad; `alphanumeric` also accepts letters.\n  type?: 'numeric' | 'alphanumeric';\n  // Draws a hyphen after this many boxes. Defaults to 3 for six boxes, none otherwise.\n  separatorAfter?: number;\n  disabled?: boolean;\n  invalid?: boolean;\n  success?: boolean;\n  label?: React.ReactNode;\n  hint?: React.ReactNode;\n  // Group name and per-box names (\"<name>\" is submitted as one hidden value).\n  name?: string;\n  autoFocus?: boolean;\n  placeholder?: string;\n}\n\nconst patterns = { numeric: /^[0-9]$/, alphanumeric: /^[0-9a-zA-Z]$/ } as const;\n\nconst InputDigit = React.forwardRef<HTMLDivElement, InputDigitProps>(\n  (\n    {\n      className,\n      length = 6,\n      value,\n      defaultValue = '',\n      onValueChange,\n      onComplete,\n      type = 'numeric',\n      separatorAfter,\n      size,\n      disabled = false,\n      invalid = false,\n      success = false,\n      label,\n      hint,\n      name,\n      autoFocus = false,\n      placeholder = '0',\n      id,\n      ...props\n    },\n    ref,\n  ) => {\n    const zero = usePreset() === 'zero';\n    size = size ?? (zero ? 'code' : undefined);\n    const l = useLabels();\n    const autoId = React.useId();\n    const baseId = id ?? autoId;\n    const labelId = `${baseId}-label`;\n    const hintId = `${baseId}-hint`;\n    const accepts = patterns[type];\n    const splitAt = separatorAfter ?? (length === 6 ? 3 : undefined);\n    const [inner, setInner] = React.useState(() => defaultValue.slice(0, length));\n    const current = (value ?? inner).slice(0, length);\n    const refs = React.useRef<Array<HTMLInputElement | null>>([]);\n\n    const chars = Array.from({ length }, (_, index) => current[index] ?? '');\n\n    const commit = (next: string[]) => {\n      const joined = next.join('');\n      setInner(joined);\n      onValueChange?.(joined);\n      if (joined.length === length && next.every(Boolean)) onComplete?.(joined);\n    };\n\n    const focusAt = (index: number) => {\n      const target = refs.current[Math.min(Math.max(index, 0), length - 1)];\n      target?.focus();\n      target?.select();\n    };\n\n    const insert = (start: number, text: string) => {\n      const incoming = Array.from(text).filter((char) => accepts.test(char));\n      if (incoming.length === 0) return;\n      const next = [...chars];\n      let index = start;\n      for (const char of incoming) {\n        if (index >= length) break;\n        next[index] = char;\n        index += 1;\n      }\n      commit(next);\n      focusAt(index >= length ? length - 1 : index);\n    };\n\n    const handleKeyDown = (index: number) => (event: React.KeyboardEvent<HTMLInputElement>) => {\n      switch (event.key) {\n        case 'Backspace': {\n          event.preventDefault();\n          const next = [...chars];\n          if (next[index]) {\n            next[index] = '';\n            commit(next);\n          } else if (index > 0) {\n            next[index - 1] = '';\n            commit(next);\n            focusAt(index - 1);\n          }\n          break;\n        }\n        case 'Delete': {\n          event.preventDefault();\n          const next = [...chars];\n          next[index] = '';\n          commit(next);\n          break;\n        }\n        case 'ArrowLeft':\n          event.preventDefault();\n          focusAt(index - 1);\n          break;\n        case 'ArrowRight':\n          event.preventDefault();\n          focusAt(index + 1);\n          break;\n        case 'Home':\n          event.preventDefault();\n          focusAt(0);\n          break;\n        case 'End':\n          event.preventDefault();\n          focusAt(length - 1);\n          break;\n        default:\n          if (event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {\n            event.preventDefault();\n            if (accepts.test(event.key)) insert(index, event.key);\n          }\n      }\n    };\n\n    const describedBy = hint ? hintId : undefined;\n\n    return (\n      <div\n        ref={ref}\n        role=\"group\"\n        aria-labelledby={label ? labelId : undefined}\n        aria-describedby={describedBy}\n        aria-disabled={disabled || undefined}\n        data-invalid={invalid || undefined}\n        className={cn('flex flex-col gap-gap-label', className)}\n        {...props}\n      >\n        {label ? (\n          <span id={labelId} className={cn('typography-input-label text-foreground', disabled && 'opacity-disabled')}>\n            {label}\n          </span>\n        ) : null}\n        <div className={cn('flex items-center', size === 'sm' || size === 'code' ? 'gap-2' : 'gap-3')}>\n          {chars.map((char, index) => (\n            <React.Fragment key={index}>\n              {splitAt && index === splitAt ? (\n                <span aria-hidden=\"true\" data-digit-separator=\"\" className=\"h-px w-3 shrink-0 bg-border-default\" />\n              ) : null}\n              <input\n                ref={(node) => {\n                  refs.current[index] = node;\n                }}\n                type=\"text\"\n                inputMode={type === 'numeric' ? 'numeric' : 'text'}\n                pattern={type === 'numeric' ? '[0-9]*' : undefined}\n                autoComplete={index === 0 ? 'one-time-code' : 'off'}\n                autoCapitalize=\"off\"\n                autoCorrect=\"off\"\n                spellCheck={false}\n                maxLength={1}\n                value={char}\n                placeholder={placeholder}\n                disabled={disabled}\n                autoFocus={autoFocus && index === 0}\n                aria-label={l.digit(index + 1, length)}\n                aria-invalid={invalid || undefined}\n                aria-describedby={describedBy}\n                data-success={success && !invalid ? 'true' : undefined}\n                className={digitVariants({ size })}\n                onFocus={(event) => event.currentTarget.select()}\n                onKeyDown={handleKeyDown(index)}\n                onChange={(event) => {\n                  const text = event.target.value;\n                  if (text) insert(index, text);\n                }}\n                onPaste={(event) => {\n                  event.preventDefault();\n                  insert(index, event.clipboardData.getData('text'));\n                }}\n              />\n            </React.Fragment>\n          ))}\n        </div>\n        {name ? <input type=\"hidden\" name={name} value={current} /> : null}\n        {hint ? (\n          <p id={hintId} className={cn('typography-input-hint', invalid ? 'text-input-foreground-hint-error' : 'text-input-foreground-hint')}>\n            {hint}\n          </p>\n        ) : null}\n      </div>\n    );\n  },\n);\nInputDigit.displayName = 'InputDigit';\n\nexport { InputDigit, digitVariants };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}