{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "confirm-otp",
  "title": "Confirm OTP",
  "description": "A callable dialog that checks a one-time password.",
  "dependencies": [
    "react-call"
  ],
  "registryDependencies": [
    "dialog",
    "button",
    "input-otp"
  ],
  "files": [
    {
      "path": "ui/confirm-otp.tsx",
      "content": "'use client'\n\n\nimport { useState } from 'react'\nimport { createCallable } from 'react-call'\n\nimport { Button } from '@/components/ui/button'\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from '@/components/ui/dialog'\nimport { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/ui/input-otp'\n\nexport interface ConfirmOtpProps {\n  title: string\n  description?: string\n  length?: number\n  confirmLabel?: string\n  cancelLabel?: string\n  verify: (code: string) => Promise<boolean>\n}\n\nexport const ConfirmOtp = createCallable<ConfirmOtpProps, boolean>(\n  ({\n    call,\n    title,\n    description,\n    length = 6,\n    confirmLabel = 'Verify',\n    cancelLabel = 'Cancel',\n    verify,\n  }) => {\n    const [value, setValue] = useState('')\n    const [error, setError] = useState<string | null>(null)\n    const [pending, setPending] = useState(false)\n\n    const submit = async (code: string) => {\n      setPending(true)\n      setError(null)\n      try {\n        if (await verify(code)) {\n          call.end(true)\n          return\n        }\n        setError('Invalid code. Try again.')\n      } catch {\n        setError('Something went wrong. Try again.')\n      }\n      setValue('')\n      setPending(false)\n    }\n\n    return (\n      <Dialog open={!call.ended} onOpenChange={(open) => !open && call.end(false)}>\n        <DialogContent showCloseButton={false}>\n          <form\n            className=\"grid gap-6\"\n            onSubmit={(event) => {\n              event.preventDefault()\n              if (value.length === length && !pending) submit(value)\n            }}\n          >\n            <DialogHeader>\n              <DialogTitle>{title}</DialogTitle>\n              {description && <DialogDescription>{description}</DialogDescription>}\n            </DialogHeader>\n            <div className=\"grid justify-items-center gap-2\">\n              <InputOTP\n                maxLength={length}\n                value={value}\n                disabled={pending}\n                onChange={setValue}\n                onComplete={submit}\n                // biome-ignore lint/a11y/noAutofocus: focus the code input the dialog exists for\n                autoFocus\n              >\n                <InputOTPGroup>\n                  {Array.from({ length }, (_, index) => (\n                    <InputOTPSlot key={index} index={index} />\n                  ))}\n                </InputOTPGroup>\n              </InputOTP>\n              <p className=\"min-h-5 text-destructive text-sm\" role=\"alert\">\n                {error}\n              </p>\n            </div>\n            <DialogFooter>\n              <Button\n                type=\"button\"\n                variant=\"outline\"\n                disabled={pending}\n                onClick={() => call.end(false)}\n              >\n                {cancelLabel}\n              </Button>\n              <Button type=\"submit\" disabled={value.length !== length || pending}>\n                {pending ? 'Verifying...' : confirmLabel}\n              </Button>\n            </DialogFooter>\n          </form>\n        </DialogContent>\n      </Dialog>\n    )\n  },\n  200,\n)\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:component"
}
