{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "code-verification",
  "title": "Code Verification",
  "description": "One-time code entry with paste, resend timing, errors, and alternate methods.",
  "dependencies": [
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "https://uaiblocks.dev/r/uai-theme.json",
    "https://uaiblocks.dev/r/inline-feedback.json",
    "https://uaiblocks.dev/r/status-banner.json",
    "https://uaiblocks.dev/r/uai-utils.json"
  ],
  "files": [
    {
      "path": "src/registry/uai/blocks/code-verification.tsx",
      "content": "\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { Check } from \"lucide-react\";\nimport {\n  type ClipboardEvent,\n  type ComponentProps,\n  type CSSProperties,\n  createContext,\n  type KeyboardEvent,\n  type ReactNode,\n  useContext,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  InlineFeedback,\n  InlineFeedbackAction,\n  type InlineFeedbackProps,\n} from \"@/components/ui/uai/inline-feedback\";\nimport {\n  StatusBanner,\n  StatusBannerContent,\n  StatusBannerIcon,\n  type StatusBannerTone,\n} from \"@/components/ui/uai/status-banner\";\nimport { cn } from \"@/lib/uai-utils\";\n\nexport const CODE_VERIFICATION_VARIANTS = [\"card\", \"split\", \"compact\"] as const;\nexport type CodeVerificationVariant = (typeof CODE_VERIFICATION_VARIANTS)[number];\nexport type CodeVerificationStatus = \"idle\" | \"verifying\" | \"invalid\" | \"expired\" | \"verified\";\n/** Resolve with nothing or \"success\" to accept the code. */\nexport type CodeVerificationResult = \"success\" | \"invalid\" | \"expired\" | undefined;\n\nexport type CodeVerificationProps = Omit<ComponentProps<\"section\">, \"defaultValue\" | \"onChange\"> & {\n  variant?: CodeVerificationVariant;\n  /** Number of characters in the code. */\n  length?: number;\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  /** Checks the code. Delivery and verification stay with the application. */\n  onVerify: (code: string) => Promise<CodeVerificationResult> | CodeVerificationResult;\n  /** Sends a new code. Throw or reject to report a failure. */\n  onResend?: () => Promise<unknown> | unknown;\n  /** Seconds before another code can be requested. The countdown starts on mount. */\n  resendAfter?: number;\n  /** Submit as soon as every character is entered. */\n  autoSubmit?: boolean;\n};\n\ntype CodeVerificationContext = {\n  id: string;\n  variant: CodeVerificationVariant;\n  length: number;\n  code: string;\n  status: CodeVerificationStatus;\n  remaining: number;\n  inputs: { current: (HTMLInputElement | null)[] };\n  change: (code: string) => void;\n  verify: (code: string) => void;\n  resend: () => Promise<void>;\n};\nconst Context = createContext<CodeVerificationContext | null>(null);\nfunction useCodeVerification(part: string) {\n  const context = useContext(Context);\n  if (!context) throw new Error(`${part} must be used within CodeVerification`);\n  return context;\n}\n\nconst codeVerificationVariants = cva(\n  \"box-border @container min-w-0 rounded-[14px] border bg-card text-[13px]/[18px] text-card-foreground\",\n  {\n    variants: {\n      variant: {\n        card: \"mx-auto max-w-110 p-[clamp(20px,6cqi,28px)]\",\n        split: \"p-[clamp(20px,4cqi,32px)]\",\n        compact: \"mx-auto max-w-90 rounded-xl p-4\",\n      },\n    },\n  },\n);\n\nconst codeVerificationActionMotion =\n  \"[transition:background-color_120ms_ease-out,color_120ms_ease-out,filter_120ms_ease-out,scale_140ms_cubic-bezier(0.23,1,0.32,1)] focus-visible:ring-0 focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-2 focus-visible:outline-ring motion-reduce:transition-none\";\n\nconst formatTime = (seconds: number) =>\n  `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, \"0\")}`;\n\n/** Collects a one-time code, verifies it through `onVerify`, and paces resend requests. */\nexport function CodeVerification({\n  variant = \"card\",\n  length = 6,\n  value,\n  defaultValue = \"\",\n  onValueChange,\n  onVerify,\n  onResend,\n  resendAfter = 30,\n  autoSubmit = true,\n  children,\n  className,\n  ...props\n}: CodeVerificationProps) {\n  const id = useId();\n  const [internal, setInternal] = useState(defaultValue);\n  const [status, setStatus] = useState<CodeVerificationStatus>(\"idle\");\n  const [remaining, setRemaining] = useState(resendAfter);\n  const inputs = useRef<(HTMLInputElement | null)[]>([]);\n  const code = (value ?? internal).slice(0, length);\n  const counting = remaining > 0;\n\n  useEffect(() => {\n    if (!counting) return;\n    const timer = window.setInterval(() => {\n      setRemaining((seconds) => Math.max(0, seconds - 1));\n    }, 1000);\n    return () => window.clearInterval(timer);\n  }, [counting]);\n\n  const change = (next: string) => {\n    if (value === undefined) setInternal(next);\n    onValueChange?.(next);\n    if (status === \"invalid\") setStatus(\"idle\");\n  };\n  const verify = async (candidate: string) => {\n    if (candidate.length < length || status === \"verifying\" || status === \"verified\") return;\n    setStatus(\"verifying\");\n    try {\n      const result = await onVerify(candidate);\n      if (result === undefined || result === \"success\") {\n        setStatus(\"verified\");\n        return;\n      }\n      setStatus(result);\n    } catch {\n      setStatus(\"invalid\");\n    }\n    if (value === undefined) setInternal(\"\");\n    onValueChange?.(\"\");\n    inputs.current[0]?.focus();\n  };\n  const resend = async () => {\n    await onResend?.();\n    setRemaining(resendAfter);\n    setStatus(\"idle\");\n    if (value === undefined) setInternal(\"\");\n    onValueChange?.(\"\");\n    inputs.current[0]?.focus();\n  };\n\n  return (\n    <Context.Provider\n      value={{\n        id,\n        variant,\n        length,\n        code,\n        status,\n        remaining,\n        inputs,\n        change: (next) => {\n          change(next);\n          if (autoSubmit && next.length === length) void verify(next);\n        },\n        verify: (candidate) => void verify(candidate),\n        resend,\n      }}\n    >\n      <section\n        aria-labelledby={`${id}-title`}\n        data-slot=\"code-verification\"\n        data-variant={variant}\n        className={cn(codeVerificationVariants({ variant }), className)}\n        {...props}\n        data-status={status}\n      >\n        <div\n          className={cn(\n            \"grid min-w-0 items-start gap-5\",\n            variant === \"split\" &&\n              \"@min-[640px]:grid-cols-[minmax(0,1fr)_minmax(0,1.1fr)] @min-[640px]:gap-x-10\",\n          )}\n        >\n          {children}\n        </div>\n      </section>\n    </Context.Provider>\n  );\n}\n\nexport function CodeVerificationHeader({ className, ...props }: ComponentProps<\"header\">) {\n  return (\n    <header\n      data-slot=\"code-verification-header\"\n      className={cn(\"grid min-w-0 gap-1.5\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function CodeVerificationTitle({ className, ...props }: ComponentProps<\"h2\">) {\n  const { id, variant } = useCodeVerification(\"CodeVerificationTitle\");\n  return (\n    <h2\n      data-slot=\"code-verification-title\"\n      className={cn(\n        \"m-0 font-semibold tracking-[-0.01em] text-balance\",\n        variant === \"compact\" ? \"text-[15px]/5\" : \"text-lg/6\",\n        className,\n      )}\n      {...props}\n      id={`${id}-title`}\n    />\n  );\n}\n\nexport function CodeVerificationDescription({ className, ...props }: ComponentProps<\"p\">) {\n  const { id } = useCodeVerification(\"CodeVerificationDescription\");\n  return (\n    <p\n      data-slot=\"code-verification-description\"\n      className={cn(\"m-0 text-pretty text-muted-foreground\", className)}\n      {...props}\n      id={`${id}-description`}\n    />\n  );\n}\n\n/** Where the code was sent, emphasised inside the description. */\nexport function CodeVerificationDestination({ className, ...props }: ComponentProps<\"strong\">) {\n  return (\n    <strong\n      data-slot=\"code-verification-destination\"\n      className={cn(\"font-medium wrap-anywhere text-foreground\", className)}\n      {...props}\n    />\n  );\n}\n\n/** The form region. Enter submits the entered code. */\nexport function CodeVerificationForm({ onSubmit, className, ...props }: ComponentProps<\"form\">) {\n  const context = useCodeVerification(\"CodeVerificationForm\");\n  return (\n    <form\n      noValidate\n      aria-labelledby={`${context.id}-title`}\n      data-slot=\"code-verification-form\"\n      className={cn(\"grid min-w-0 gap-3.5\", className)}\n      {...props}\n      onSubmit={(event) => {\n        onSubmit?.(event);\n        if (event.defaultPrevented) return;\n        event.preventDefault();\n        if (context.code.length < context.length) {\n          context.inputs.current[context.code.length]?.focus();\n          return;\n        }\n        context.verify(context.code);\n      }}\n    />\n  );\n}\n\nexport type CodeVerificationInputProps = Omit<ComponentProps<\"fieldset\">, \"children\"> & {\n  /** Accessible name of the group. */\n  label?: string;\n  /** Restrict entry to digits (the default) or allow letters too. */\n  mode?: \"numeric\" | \"alphanumeric\";\n};\n\n/**\n * One input per character. Typing advances, Backspace steps back, arrows and Home/End move,\n * and pasting a full code fills every box. The first box carries autocomplete=\"one-time-code\".\n */\nexport function CodeVerificationInput({\n  label = \"Verification code\",\n  mode = \"numeric\",\n  className,\n  style,\n  ...props\n}: CodeVerificationInputProps) {\n  const context = useCodeVerification(\"CodeVerificationInput\");\n  const { length, code, status, inputs, variant } = context;\n  const invalid = status === \"invalid\" || status === \"expired\";\n  const locked = status === \"verifying\" || status === \"verified\";\n  const size = variant === \"compact\" ? 36 : 44;\n  const clean = (text: string) =>\n    mode === \"numeric\" ? text.replace(/\\D/g, \"\") : text.replace(/[^0-9a-z]/gi, \"\").toUpperCase();\n  const focusAt = (index: number) => {\n    const target = inputs.current[Math.max(0, Math.min(length - 1, index))];\n    target?.focus();\n    target?.select();\n  };\n  // Focus after a code change waits for the render, so the focus guard sees the new code.\n  const pendingFocus = useRef<number | null>(null);\n  useLayoutEffect(() => {\n    if (pendingFocus.current === null) return;\n    focusAt(pendingFocus.current);\n    pendingFocus.current = null;\n  });\n  const changeAndFocus = (next: string, index: number) => {\n    pendingFocus.current = index;\n    context.change(next);\n  };\n  const fill = (start: number, text: string) => {\n    const chars = clean(text).slice(0, length - start);\n    if (!chars) return;\n    changeAndFocus(\n      (code.slice(0, start) + chars + code.slice(start + chars.length)).slice(0, length),\n      Math.min(start + chars.length, length - 1),\n    );\n  };\n  const onKeyDown = (index: number) => (event: KeyboardEvent<HTMLInputElement>) => {\n    if (event.key === \"Backspace\") {\n      event.preventDefault();\n      if (code[index]) {\n        changeAndFocus(code.slice(0, index) + code.slice(index + 1), index);\n      } else if (index > 0) {\n        changeAndFocus(code.slice(0, index - 1) + code.slice(index), index - 1);\n      }\n    } else if (event.key === \"ArrowLeft\") {\n      event.preventDefault();\n      focusAt(index - 1);\n    } else if (event.key === \"ArrowRight\") {\n      event.preventDefault();\n      focusAt(Math.min(index + 1, code.length));\n    } else if (event.key === \"Home\") {\n      event.preventDefault();\n      focusAt(0);\n    } else if (event.key === \"End\") {\n      event.preventDefault();\n      focusAt(Math.min(code.length, length - 1));\n    }\n  };\n  return (\n    <fieldset\n      aria-label={label}\n      aria-describedby={`${context.id}-description ${context.id}-message`}\n      data-slot=\"code-verification-input\"\n      className={cn(\n        \"m-0 grid min-w-0 border-0 p-0\",\n        variant === \"split\" ? \"justify-start\" : \"justify-center\",\n        variant === \"compact\" ? \"gap-1.5\" : \"gap-2\",\n        // Replays on each failed attempt: the status passes through \"verifying\" in between.\n        status === \"invalid\" && \"animate-shake motion-reduce:animate-none\",\n        className,\n      )}\n      {...props}\n      style={{ gridTemplateColumns: `repeat(${length}, minmax(0, ${size}px))`, ...style }}\n    >\n      {Array.from({ length }, (_, index) => {\n        const char = code[index] ?? \"\";\n        return (\n          <input\n            // biome-ignore lint/suspicious/noArrayIndexKey: positions are stable slots\n            key={index}\n            ref={(node) => {\n              inputs.current[index] = node;\n            }}\n            aria-label={`Character ${index + 1} of ${length}`}\n            aria-invalid={invalid || undefined}\n            data-slot=\"code-verification-cell\"\n            className={cn(\n              \"box-border h-12 w-full min-w-0 rounded-[10px] border p-0 text-center text-[20px] font-medium tabular-nums caret-foreground outline-none focus:shadow-[0_0_0_3px_color-mix(in_oklab,var(--primary)_22%,transparent)] data-filled:animate-in data-filled:zoom-in-95 data-filled:duration-150 data-filled:ease-out-quint motion-reduce:transition-none motion-reduce:animate-none\",\n              variant === \"compact\" && \"h-10 rounded-lg text-[18px]\",\n              status === \"verified\"\n                ? // The success wave staggers per cell through `--i`; only this state carries a delay.\n                  \"border-transparent bg-[color-mix(in_oklab,var(--success)_8%,var(--card))] text-success-ink [transition:border-color_200ms_ease-out,background-color_200ms_ease-out,color_200ms_ease-out] [transition-delay:calc(var(--i)*35ms)]\"\n                : cn(\n                    \"text-foreground [transition:border-color_120ms_ease-out,background-color_120ms_ease-out,box-shadow_120ms_ease-out,color_120ms_ease-out] hover:not-focus:border-border\",\n                    locked\n                      ? \"border-transparent bg-muted\"\n                      : invalid\n                        ? \"border-destructive bg-[color-mix(in_oklab,var(--destructive)_6%,var(--card))]\"\n                        : cn(\n                            \"focus:border-primary\",\n                            char\n                              ? \"border-border-strong bg-card\"\n                              : \"border-transparent bg-background\",\n                          ),\n                  ),\n            )}\n            data-filled={char ? \"\" : undefined}\n            style={{ \"--i\": index } as CSSProperties}\n            autoComplete={index === 0 ? \"one-time-code\" : \"off\"}\n            inputMode={mode === \"numeric\" ? \"numeric\" : \"text\"}\n            pattern={mode === \"numeric\" ? \"[0-9]*\" : undefined}\n            autoCapitalize=\"characters\"\n            spellCheck={false}\n            disabled={status === \"verified\"}\n            readOnly={locked}\n            value={char}\n            onFocus={(event) => {\n              if (index > code.length) focusAt(code.length);\n              else event.currentTarget.select();\n            }}\n            onChange={(event) => {\n              // A box can briefly hold its old character plus the new one, or a whole autofilled code.\n              let typed = clean(event.target.value);\n              if (char && typed.length === 2) {\n                typed = typed.startsWith(char) ? typed.slice(1) : typed.slice(0, 1);\n              }\n              if (typed) fill(index, typed);\n            }}\n            onPaste={(event: ClipboardEvent<HTMLInputElement>) => {\n              event.preventDefault();\n              fill(index, event.clipboardData.getData(\"text\"));\n            }}\n            onKeyDown={onKeyDown(index)}\n          />\n        );\n      })}\n    </fieldset>\n  );\n}\n\nconst messageTones: Record<\"invalid\" | \"expired\" | \"verified\", StatusBannerTone> = {\n  invalid: \"error\",\n  expired: \"warning\",\n  verified: \"success\",\n};\n\n/** Shows its content only for the matching verification status. */\nexport function CodeVerificationMessage({\n  status,\n  children,\n  ...props\n}: Omit<ComponentProps<typeof StatusBanner>, \"tone\"> & {\n  status: \"invalid\" | \"expired\" | \"verified\";\n}) {\n  const context = useCodeVerification(\"CodeVerificationMessage\");\n  if (context.status !== status) return null;\n  return (\n    <StatusBanner\n      variant=\"tinted\"\n      {...props}\n      id={`${context.id}-message`}\n      tone={messageTones[status]}\n    >\n      <StatusBannerIcon />\n      <StatusBannerContent>{children}</StatusBannerContent>\n    </StatusBanner>\n  );\n}\n\nexport function CodeVerificationSubmit({\n  pendingLabel = \"Verifying…\",\n  verifiedLabel = \"Verified\",\n  children,\n  className,\n  ...props\n}: ComponentProps<\"button\"> & { pendingLabel?: ReactNode; verifiedLabel?: ReactNode }) {\n  const context = useCodeVerification(\"CodeVerificationSubmit\");\n  const pending = context.status === \"verifying\";\n  const verified = context.status === \"verified\";\n  const ready = context.code.length === context.length && context.status !== \"verified\";\n  return (\n    <Button\n      type=\"submit\"\n      data-slot=\"code-verification-submit\"\n      className={cn(\n        \"rounded-full border-0 px-4 py-0 text-[13px] has-[>svg]:px-4\",\n        codeVerificationActionMotion,\n        context.variant === \"compact\" ? \"h-7.5\" : \"h-8.5\",\n        verified\n          ? \"cursor-default gap-1.5 bg-success/14 text-success-ink hover:bg-success/14\"\n          : ready || pending\n            ? \"bg-primary text-primary-foreground hover:bg-primary hover:brightness-108 active:scale-[0.97] motion-reduce:active:scale-100\"\n            : \"bg-muted text-subtle-foreground hover:bg-muted\",\n        !verified &&\n          (pending ? \"cursor-progress\" : ready ? \"cursor-pointer\" : \"cursor-not-allowed\"),\n        className,\n      )}\n      {...props}\n      aria-disabled={!ready || pending || undefined}\n      aria-busy={pending || undefined}\n      data-state={verified ? \"verified\" : ready || pending ? \"ready\" : \"idle\"}\n    >\n      {verified ? (\n        <>\n          <Check\n            className=\"size-3.5 animate-in fade-in-0 zoom-in-50 duration-200 ease-out-quint motion-reduce:animate-none\"\n            strokeWidth={2.2}\n            aria-hidden=\"true\"\n          />\n          {verifiedLabel}\n        </>\n      ) : pending ? (\n        <span className=\"animate-shimmer bg-[linear-gradient(90deg,color-mix(in_oklab,currentColor_55%,transparent)_0%,color-mix(in_oklab,currentColor_55%,transparent)_35%,currentColor_50%,color-mix(in_oklab,currentColor_55%,transparent)_65%,color-mix(in_oklab,currentColor_55%,transparent)_100%)] bg-[length:200%_100%] bg-clip-text [-webkit-text-fill-color:transparent] motion-reduce:animate-none motion-reduce:bg-none motion-reduce:[-webkit-text-fill-color:currentColor]\">\n          {pendingLabel}\n        </span>\n      ) : (\n        children\n      )}\n    </Button>\n  );\n}\n\n/** Groups the resend action and its Inline Feedback status. */\nexport function CodeVerificationResend({ className, ...props }: InlineFeedbackProps) {\n  useCodeVerification(\"CodeVerificationResend\");\n  return (\n    <InlineFeedback\n      duration={4000}\n      data-slot=\"code-verification-resend\"\n      className={cn(\"justify-center\", className)}\n      {...props}\n    />\n  );\n}\n\n/** Requests a new code. Disabled while the countdown runs, which it shows as m:ss. */\nexport function CodeVerificationResendButton({\n  children,\n  className,\n  ...props\n}: Omit<ComponentProps<typeof InlineFeedbackAction>, \"onAction\">) {\n  const context = useCodeVerification(\"CodeVerificationResendButton\");\n  const waiting = context.remaining > 0;\n  return (\n    <InlineFeedbackAction\n      className={cn(\n        \"border-0 bg-transparent px-1 decoration-border-strong underline-offset-3\",\n        waiting\n          ? \"cursor-default text-subtle-foreground no-underline\"\n          : \"cursor-pointer text-foreground underline\",\n        className,\n      )}\n      {...props}\n      disabled={waiting || context.status === \"verified\"}\n      onAction={context.resend}\n    >\n      {children}\n      {waiting && <span className=\"tabular-nums\"> in {formatTime(context.remaining)}</span>}\n    </InlineFeedbackAction>\n  );\n}\n\n/** Other ways to verify, such as a call, backup code, or authenticator app. */\nexport function CodeVerificationAlternatives({ className, ...props }: ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"code-verification-alternatives\"\n      className={cn(\n        \"flex flex-wrap items-center justify-center gap-1.5 border-t pt-4 text-[12px] text-subtle-foreground\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function CodeVerificationAlternative({ className, ...props }: ComponentProps<\"button\">) {\n  return (\n    <Button\n      type=\"button\"\n      variant=\"secondary\"\n      data-slot=\"code-verification-alternative\"\n      className={cn(\n        \"h-7 cursor-pointer rounded-full border-0 bg-muted px-3 py-0 text-[12.5px] text-foreground hover:bg-[color-mix(in_oklab,var(--muted)_85%,var(--foreground))] active:scale-[0.97] motion-reduce:active:scale-100 has-[>svg]:px-3\",\n        codeVerificationActionMotion,\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/uai/code-verification.tsx"
    }
  ],
  "categories": [
    "authentication",
    "onboarding",
    "forms"
  ],
  "type": "registry:block"
}