{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "verify-1",
  "title": "Verify 1: one-time code with resend",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A code-entry screen: masked email, six slots that accept typing and pasting, automatic submit on the last digit, shake and alert on a wrong code, a resend countdown and an animated success state.",
  "dependencies": [
    "lucide-react",
    "motion@^12"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/button.json",
    "https://ui.ballmac.com/r/input-otp.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/verify-1/verify-1.tsx",
      "content": "// Ballmac UI: Verify 1. https://ui.ballmac.com/blocks/verify-1\n\"use client\"\n\nimport * as React from \"react\"\nimport { Check, ShieldCheck } from \"lucide-react\"\nimport { motion, useReducedMotion } from \"motion/react\"\n\nimport { Button, buttonVariants } from \"@/components/ballmac/button\"\nimport { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from \"@/components/ballmac/input-otp\"\nimport { cn } from \"@/lib/utils\"\n\ntype Verify1Props = Omit<React.ComponentProps<\"section\">, \"title\" | \"onSubmit\"> & {\n  /** Where the code was sent. Shown partly masked. */\n  email?: string\n  /** Heading. */\n  title?: string\n  /** Digits in the code. */\n  length?: number\n  /**\n   * Called with the full code, as soon as the last digit is typed or pasted. Throw to show \"wrong code\"; resolve to show success.\n   * Without it, any code except all zeros is accepted after a short pause, so you can try the states.\n   */\n  onVerify?: (code: string) => void | Promise<void>\n  /** Called by \"Resend code\". Without it the countdown simply restarts. */\n  onResend?: () => void | Promise<void>\n  /** Seconds before \"Resend code\" works again. */\n  resendAfter?: number\n  /** Called by \"Use a different email\". Pass null to hide it. */\n  onChangeEmail?: (() => void) | null\n  /** Where the success button goes. */\n  continueHref?: string\n}\n\nfunction maskEmail(email: string) {\n  const [name, domain] = email.split(\"@\")\n  if (!domain) return email\n  return `${name.slice(0, 1)}${\"•\".repeat(Math.max(name.length - 1, 2))}@${domain}`\n}\n\nfunction Verify1({\n  email = \"jordan@acme.com\",\n  title = \"Enter your code\",\n  length = 6,\n  onVerify,\n  onResend,\n  resendAfter = 30,\n  onChangeEmail = () => {},\n  continueHref = \"#\",\n  className,\n  ...props\n}: Verify1Props) {\n  const reduce = useReducedMotion()\n  const [code, setCode] = React.useState(\"\")\n  const [state, setState] = React.useState<\"idle\" | \"checking\" | \"wrong\" | \"done\">(\"idle\")\n  const [wait, setWait] = React.useState(resendAfter)\n  const [resent, setResent] = React.useState(false)\n  const headingRef = React.useRef<HTMLHeadingElement>(null)\n  const otpRef = React.useRef<HTMLInputElement>(null)\n\n  React.useEffect(() => {\n    if (wait <= 0) return\n    const id = window.setTimeout(() => setWait((w) => w - 1), 1000)\n    return () => window.clearTimeout(id)\n  }, [wait])\n\n  async function check(value: string) {\n    setState(\"checking\")\n    try {\n      if (onVerify) await onVerify(value)\n      else {\n        await new Promise((r) => setTimeout(r, 800))\n        if (/^0+$/.test(value)) throw new Error(\"wrong\")\n      }\n      setState(\"done\")\n      window.setTimeout(() => headingRef.current?.focus(), 0)\n    } catch {\n      setState(\"wrong\")\n      setCode(\"\")\n      window.setTimeout(() => otpRef.current?.focus(), 0)\n    }\n  }\n\n  async function resend() {\n    setResent(true)\n    setCode(\"\")\n    setState(\"idle\")\n    setWait(resendAfter)\n    await onResend?.()\n    otpRef.current?.focus()\n  }\n\n  const half = Math.ceil(length / 2)\n  const slots = Array.from({ length }, (_, i) => i)\n  return (\n    <section data-slot=\"verify-1\" className={cn(\"flex min-h-[36rem] items-center justify-center px-4 py-14\", className)} {...props}>\n      <div className=\"bg-card w-full max-w-md rounded-3xl border p-7 text-center shadow-[0_30px_80px_-50px_rgb(0_0_0/0.4)] sm:p-9\">\n        {state === \"done\" ? (\n          <div className=\"flex flex-col items-center py-4\">\n            <motion.span\n              aria-hidden=\"true\"\n              initial={reduce ? false : { scale: 0.5, opacity: 0 }}\n              animate={{ scale: 1, opacity: 1 }}\n              transition={{ type: \"spring\", stiffness: 300, damping: 18 }}\n              className=\"bg-chart-2/15 flex size-14 items-center justify-center rounded-full\"\n            >\n              <Check className=\"size-7\" />\n            </motion.span>\n            <h1 ref={headingRef} tabIndex={-1} className=\"mt-6 text-2xl font-semibold tracking-[-0.03em] outline-none\">You’re verified</h1>\n            <p role=\"status\" className=\"text-muted-foreground mt-2 max-w-xs text-pretty\">Thanks for confirming it’s you. Your account is ready to use.</p>\n            <a href={continueHref} className={buttonVariants({ size: \"lg\", shape: \"pill\", className: \"mt-8 w-full\" })}>Continue</a>\n          </div>\n        ) : (\n          <>\n            <span aria-hidden=\"true\" className=\"bg-muted mx-auto flex size-12 items-center justify-center rounded-2xl\"><ShieldCheck className=\"size-6\" /></span>\n            <h1 ref={headingRef} tabIndex={-1} className=\"mt-6 text-2xl font-semibold tracking-[-0.03em] outline-none\">{title}</h1>\n            <p className=\"text-muted-foreground mt-2 text-pretty\">We sent a {length}-digit code to <span className=\"text-foreground font-medium\">{maskEmail(email)}</span>. It expires in 10 minutes.</p>\n\n            <motion.div\n              className=\"mt-8 flex justify-center\"\n              animate={state === \"wrong\" && !reduce ? { x: [0, -8, 8, -6, 6, -3, 0] } : { x: 0 }}\n              transition={{ duration: 0.4 }}\n            >\n              <InputOTP\n                ref={otpRef}\n                maxLength={length}\n                value={code}\n                autoFocus\n                disabled={state === \"checking\"}\n                aria-label={`${length}-digit verification code`}\n                aria-invalid={state === \"wrong\" || undefined}\n                aria-describedby=\"verify-1-status\"\n                onChange={(v) => { setCode(v); if (state === \"wrong\") setState(\"idle\") }}\n                onComplete={check}\n              >\n                <InputOTPGroup>{slots.slice(0, half).map((i) => <InputOTPSlot key={i} index={i} invalid={state === \"wrong\"} />)}</InputOTPGroup>\n                <InputOTPSeparator />\n                <InputOTPGroup>{slots.slice(half).map((i) => <InputOTPSlot key={i} index={i} invalid={state === \"wrong\"} />)}</InputOTPGroup>\n              </InputOTP>\n            </motion.div>\n\n            <p id=\"verify-1-status\" role={state === \"wrong\" ? \"alert\" : \"status\"} className={cn(\"mt-4 min-h-5 text-sm\", state === \"wrong\" ? \"text-destructive\" : \"text-muted-foreground\")}>\n              {state === \"checking\" && \"Checking your code…\"}\n              {state === \"wrong\" && \"That code isn’t right. Check the email and try again.\"}\n              {state === \"idle\" && resent && \"A new code is on its way.\"}\n            </p>\n\n            <p className=\"text-muted-foreground mt-6 text-sm\">\n              {wait > 0 ? <>Didn’t get it? You can resend in <span className=\"tabular-nums\">{wait}</span>s.</> : \"Didn’t get the code?\"}\n            </p>\n            <div className=\"mt-1 flex justify-center gap-2\">\n              <Button variant=\"ghost\" size=\"sm\" disabled={wait > 0 || state === \"checking\"} onClick={resend}>Resend code</Button>\n              {onChangeEmail && <Button variant=\"ghost\" size=\"sm\" onClick={onChangeEmail}>Use a different email</Button>}\n            </div>\n          </>\n        )}\n      </div>\n    </section>\n  )\n}\n\nexport { Verify1, type Verify1Props }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/verify-1/verify-1.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "verify",
      "otp",
      "code",
      "two-factor",
      "email",
      "resend"
    ],
    "version": "1.0.0",
    "updated": "2026-10-01",
    "ai": {
      "summary": "Email or SMS code verification. Pass email, length, onVerify(code) (throw for a wrong code) and onResend. Without onVerify any code except all zeros succeeds, so you can try every state.",
      "whenToUse": [
        "Email confirmation after signup",
        "Two-factor sign-in and sensitive actions"
      ],
      "whenNotToUse": [
        "Magic links that need no typing"
      ],
      "composesWith": [
        "signup-1",
        "login-2",
        "forgot-password-1",
        "onboarding-1"
      ],
      "a11y": [
        {
          "keys": "Typing or pasting",
          "action": "Fills the slots; the last digit submits automatically"
        },
        {
          "keys": "Wrong code",
          "action": "Announced as an alert, the field is marked invalid and focus returns to it"
        },
        {
          "keys": "Resend",
          "action": "The countdown is shown as text; the button is disabled until it ends"
        }
      ],
      "customization": [
        "length: digits in the code",
        "onVerify, onResend, onChangeEmail (null hides)",
        "resendAfter: seconds"
      ]
    },
    "examples": [
      "verify-1-demo",
      "verify-1-four"
    ],
    "url": "https://ui.ballmac.com/components/verify-1"
  },
  "categories": [
    "blocks",
    "auth"
  ],
  "type": "registry:block"
}