{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "forgot-password-1",
  "title": "Forgot Password 1: request, resend and reset",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "The whole password-recovery flow in one card: request a link, check-your-email with a resend countdown, choose a new password with confirmation and strength, and a success screen. Focus moves to each new heading.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/button.json",
    "https://ui.ballmac.com/r/field.json",
    "https://ui.ballmac.com/r/input.json",
    "https://ui.ballmac.com/r/password-input.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/forgot-password-1/forgot-password-1.tsx",
      "content": "// Ballmac UI: Forgot Password 1. https://ui.ballmac.com/blocks/forgot-password-1\n\"use client\"\n\nimport * as React from \"react\"\nimport { ArrowLeft, ArrowRight, Check, KeyRound, MailCheck } from \"lucide-react\"\n\nimport { Button, buttonVariants } from \"@/components/ballmac/button\"\nimport { Field, FieldDescription, FieldError, FieldGroup, FieldLabel, useFieldControl } from \"@/components/ballmac/field\"\nimport { Input } from \"@/components/ballmac/input\"\nimport { PasswordInput } from \"@/components/ballmac/password-input\"\nimport { cn } from \"@/lib/utils\"\n\ntype ForgotPassword1View = \"request\" | \"sent\" | \"reset\" | \"done\"\n\ntype ForgotPassword1Props = Omit<React.ComponentProps<\"section\">, \"onSubmit\"> & {\n  /** Which screen to start on. Use \"reset\" on the page the emailed link opens. */\n  initialView?: ForgotPassword1View\n  /** Email shown on the confirmation screen when you start past the request. */\n  defaultEmail?: string\n  /** Called with the email. Throw to show an error; resolve to show the confirmation. */\n  onRequest?: (email: string) => void | Promise<void>\n  /** Called with the new password. Throw to show an error; resolve to show the success screen. */\n  onReset?: (password: string) => void | Promise<void>\n  /** Seconds before \"Resend\" works again. */\n  resendAfter?: number\n  /** Where \"Back to sign in\" goes. */\n  loginHref?: string\n}\n\nfunction FieldInput(props: React.ComponentProps<typeof Input>) {\n  return <Input {...useFieldControl()} {...props} />\n}\nfunction FieldPassword(props: React.ComponentProps<typeof PasswordInput>) {\n  return <PasswordInput {...useFieldControl()} {...props} />\n}\n\nconst emailPattern = /^\\S+@\\S+\\.\\S+$/\n\nfunction Icon({ children }: { children: React.ReactNode }) {\n  return <span aria-hidden=\"true\" className=\"bg-muted flex size-12 items-center justify-center rounded-2xl [&_svg]:size-6\">{children}</span>\n}\n\nfunction ForgotPassword1({\n  initialView = \"request\",\n  defaultEmail = \"\",\n  onRequest,\n  onReset,\n  resendAfter = 30,\n  loginHref = \"#\",\n  className,\n  ...props\n}: ForgotPassword1Props) {\n  const [view, setView] = React.useState<ForgotPassword1View>(initialView)\n  const [email, setEmail] = React.useState(defaultEmail)\n  const [password, setPassword] = React.useState(\"\")\n  const [confirm, setConfirm] = React.useState(\"\")\n  const [touched, setTouched] = React.useState<Record<string, boolean>>({})\n  const [busy, setBusy] = React.useState(false)\n  const [failed, setFailed] = React.useState(false)\n  const [wait, setWait] = React.useState(initialView === \"sent\" ? resendAfter : 0)\n  const [resent, setResent] = React.useState(false)\n  const headingRef = React.useRef<HTMLHeadingElement>(null)\n\n  // Count the resend cooldown down once a second while it is running.\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  // Move focus to the new heading when the screen changes, so keyboard and screen reader users land on it.\n  const first = React.useRef(true)\n  React.useEffect(() => {\n    if (first.current) {\n      first.current = false\n      return\n    }\n    headingRef.current?.focus()\n  }, [view])\n\n  const emailError = touched.email && !emailPattern.test(email) ? \"Enter a full email address, like name@company.com.\" : undefined\n  const passwordError = touched.password && password.length < 8 ? \"Use at least 8 characters.\" : undefined\n  const confirmError = touched.confirm && confirm !== password ? \"The passwords don’t match.\" : undefined\n\n  async function request(e: React.FormEvent) {\n    e.preventDefault()\n    setTouched((t) => ({ ...t, email: true }))\n    if (!emailPattern.test(email)) return\n    setBusy(true)\n    setFailed(false)\n    try {\n      if (onRequest) await onRequest(email)\n      else await new Promise((r) => setTimeout(r, 800))\n      setWait(resendAfter)\n      setView(\"sent\")\n    } catch {\n      setFailed(true)\n    } finally {\n      setBusy(false)\n    }\n  }\n\n  async function resend() {\n    setBusy(true)\n    try {\n      if (onRequest) await onRequest(email)\n      else await new Promise((r) => setTimeout(r, 500))\n      setResent(true)\n      setWait(resendAfter)\n    } finally {\n      setBusy(false)\n    }\n  }\n\n  async function reset(e: React.FormEvent) {\n    e.preventDefault()\n    setTouched((t) => ({ ...t, password: true, confirm: true }))\n    if (password.length < 8 || confirm !== password) return\n    setBusy(true)\n    setFailed(false)\n    try {\n      if (onReset) await onReset(password)\n      else await new Promise((r) => setTimeout(r, 800))\n      setView(\"done\")\n    } catch {\n      setFailed(true)\n    } finally {\n      setBusy(false)\n    }\n  }\n\n  const heading = \"text-2xl font-semibold tracking-[-0.03em] outline-none\"\n  return (\n    <section data-slot=\"forgot-password-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 shadow-[0_30px_80px_-50px_rgb(0_0_0/0.4)] sm:p-9\">\n        {view === \"request\" && (\n          <>\n            <Icon><KeyRound /></Icon>\n            <h1 ref={headingRef} tabIndex={-1} className={cn(heading, \"mt-6\")}>Forgot your password?</h1>\n            <p className=\"text-muted-foreground mt-2 text-pretty\">Enter the email you signed up with and we’ll send you a link to choose a new one.</p>\n            <form noValidate onSubmit={request} aria-label=\"Request a password reset\" className=\"mt-7\">\n              <FieldGroup>\n                {failed && <p role=\"alert\" className=\"border-destructive/40 text-destructive rounded-lg border px-3 py-2.5 text-sm\">We couldn’t send the email. Please try again.</p>}\n                <Field invalid={!!emailError}>\n                  <FieldLabel>Email</FieldLabel>\n                  <FieldInput type=\"email\" autoComplete=\"email\" placeholder=\"you@company.com\" value={email} onChange={(e) => setEmail(e.target.value)} onBlur={() => setTouched((t) => ({ ...t, email: true }))} />\n                  <FieldError errors={[emailError]} />\n                </Field>\n                <Button type=\"submit\" size=\"lg\" shape=\"pill\" loading={busy} className=\"w-full\">Send reset link <ArrowRight  className=\"rtl:rotate-180\"/></Button>\n              </FieldGroup>\n            </form>\n            <a href={loginHref} className=\"text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 mt-6 inline-flex items-center gap-1.5 rounded-sm text-sm outline-none focus-visible:ring-[3px]\"><ArrowLeft className=\"size-4 rtl:rotate-180\" aria-hidden=\"true\" />Back to sign in</a>\n          </>\n        )}\n\n        {view === \"sent\" && (\n          <>\n            <Icon><MailCheck /></Icon>\n            <h1 ref={headingRef} tabIndex={-1} className={cn(heading, \"mt-6\")}>Check your email</h1>\n            <p className=\"text-muted-foreground mt-2 text-pretty\">If an account exists for <span className=\"text-foreground font-medium break-all\">{email || \"that address\"}</span>, a reset link is on its way. It expires in one hour.</p>\n            <a href=\"mailto:\" className={buttonVariants({ size: \"lg\", shape: \"pill\", className: \"mt-7 w-full\" })}>Open email app</a>\n            <p role=\"status\" className=\"text-muted-foreground mt-5 text-center text-sm\">\n              {resent && wait > 0 ? \"Sent again. \" : \"\"}\n              {wait > 0 ? <>You can resend in <span className=\"tabular-nums\">{wait}</span>s.</> : \"Didn’t get it?\"}\n            </p>\n            <div className=\"mt-2 flex justify-center gap-2\">\n              <Button variant=\"ghost\" size=\"sm\" disabled={wait > 0 || busy} loading={busy} onClick={resend}>Resend email</Button>\n              <Button variant=\"ghost\" size=\"sm\" onClick={() => { setView(\"request\"); setResent(false) }}>Use a different email</Button>\n            </div>\n          </>\n        )}\n\n        {view === \"reset\" && (\n          <>\n            <Icon><KeyRound /></Icon>\n            <h1 ref={headingRef} tabIndex={-1} className={cn(heading, \"mt-6\")}>Choose a new password</h1>\n            <p className=\"text-muted-foreground mt-2 text-pretty\">Pick something you haven’t used elsewhere. You’ll be signed out of other devices.</p>\n            <form noValidate onSubmit={reset} aria-label=\"Choose a new password\" className=\"mt-7\">\n              <FieldGroup>\n                {failed && <p role=\"alert\" className=\"border-destructive/40 text-destructive rounded-lg border px-3 py-2.5 text-sm\">This link has expired. Request a new one and try again.</p>}\n                <Field invalid={!!passwordError}>\n                  <FieldLabel>New password</FieldLabel>\n                  <FieldPassword label=\"New password\" value={password} onValueChange={setPassword} onBlur={() => setTouched((t) => ({ ...t, password: true }))} />\n                  <FieldError errors={[passwordError]} />\n                  {!password && <FieldDescription>At least 8 characters.</FieldDescription>}\n                </Field>\n                <Field invalid={!!confirmError}>\n                  <FieldLabel>Confirm password</FieldLabel>\n                  <FieldPassword label=\"Confirm password\" showStrength={false} value={confirm} onValueChange={setConfirm} onBlur={() => setTouched((t) => ({ ...t, confirm: true }))} />\n                  <FieldError errors={[confirmError]} />\n                </Field>\n                <Button type=\"submit\" size=\"lg\" shape=\"pill\" loading={busy} className=\"w-full\">Update password</Button>\n              </FieldGroup>\n            </form>\n          </>\n        )}\n\n        {view === \"done\" && (\n          <div className=\"flex flex-col items-center py-4 text-center\">\n            <span aria-hidden=\"true\" className=\"bg-chart-2/15 flex size-14 items-center justify-center rounded-full\"><Check className=\"size-7\" /></span>\n            <h1 ref={headingRef} tabIndex={-1} className={cn(heading, \"mt-6\")}>Password updated</h1>\n            <p role=\"status\" className=\"text-muted-foreground mt-2 max-w-xs text-pretty\">You can now sign in with your new password.</p>\n            <a href={loginHref} className={buttonVariants({ size: \"lg\", shape: \"pill\", className: \"mt-8 w-full\" })}>Continue to sign in</a>\n          </div>\n        )}\n      </div>\n    </section>\n  )\n}\n\nexport { ForgotPassword1, type ForgotPassword1Props, type ForgotPassword1View }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/forgot-password-1/forgot-password-1.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "forgot password",
      "reset password",
      "auth",
      "recovery",
      "email",
      "countdown"
    ],
    "version": "1.0.0",
    "updated": "2026-10-01",
    "ai": {
      "summary": "Use it on /forgot-password (initialView 'request') and on the page the emailed link opens (initialView 'reset'). Pass onRequest(email) and onReset(password); throw to show an error.",
      "whenToUse": [
        "Any app with passwords",
        "The page an emailed reset link opens"
      ],
      "whenNotToUse": [
        "Passwordless-only products"
      ],
      "composesWith": [
        "login-2",
        "login-1",
        "verify-1"
      ],
      "a11y": [
        {
          "keys": "Screen changes",
          "action": "Focus moves to the new heading so the change is announced"
        },
        {
          "keys": "Resend",
          "action": "The countdown is announced politely; the button is disabled until it ends"
        }
      ],
      "customization": [
        "initialView: 'request' | 'sent' | 'reset' | 'done'",
        "onRequest / onReset async handlers",
        "resendAfter: cooldown in seconds"
      ]
    },
    "examples": [
      "forgot-password-1-demo",
      "forgot-password-1-reset"
    ],
    "url": "https://ui.ballmac.com/components/forgot-password-1"
  },
  "categories": [
    "blocks",
    "auth"
  ],
  "type": "registry:block"
}