{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "auth",
  "title": "Auth",
  "author": "@fma-ds/ds",
  "description": "The frame of sign-in, sign-up and password recovery screens. The fields are the app's (`TextField`, `password-input`, `input-otp`, `password-rules`).\n\n- **`AuthLayout`**: puts the `logo` on top and the content centered. With `brand` (product message, illustration) the page splits in two on wide screens, the brand area on a muted surface or, with `brandSurface=\"brand\"`, the theme's brand color; without it, the column sits on a muted page.\n- **`AuthCard`**: the form's card — `title` as the page's `h1`, `description`, your fields, and a centered `footer` for the links between screens.\n- **`AuthSubmit`**: the full-width submit button, with a spinner while `isPending`.\n- **`ResendCode`**: \"Resend code\" behind a cooldown (`cooldown` seconds, 60 by default, started on mount). It's disabled with an `m:ss` countdown, then `onResend` (pending while its promise runs) restarts it.\n\nThe stories show sign-in, forgot password and a two-step reset (the server checks the code, then the new password follows the rules).\n\nBuilt-in text is in English; translate `ResendCode` with `i18n.labels`.",
  "dependencies": [
    "cn@^0.5.0"
  ],
  "registryDependencies": [
    "@fma-ds/button",
    "@fma-ds/card",
    "@fma-ds/spinner",
    "@fma-ds/brand"
  ],
  "files": [
    {
      "path": "src/blocks/auth/auth.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"cn\";\nimport * as React from \"react\";\nimport { Button } from \"@/core/button/button\";\nimport { Card, CardContent, CardDescription, CardHeader } from \"@/core/card/card\";\nimport { Spinner } from \"@/core/spinner/spinner\";\nimport { brandSurface } from \"@/lib/brand\";\n\n/**\n * The page around a sign-in, sign-up or recovery card: the `logo` on top and\n * the content centered. With `brand`, the page splits in two on wide screens\n * and the brand area (illustration, product message) fills the other half, on\n * a muted surface or, with `brandSurface=\"brand\"`, the theme's brand color;\n * without it, the column sits centered on a muted page.\n */\nfunction AuthLayout({\n  logo,\n  brand,\n  brandSurface: surface = \"muted\",\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  logo?: React.ReactNode;\n  brand?: React.ReactNode;\n  /** The brand area's surface: `muted` (default) or the theme's `brand` color. */\n  brandSurface?: \"muted\" | \"brand\";\n}) {\n  return (\n    <div\n      data-slot=\"auth-layout\"\n      className={cn(\n        \"grid min-h-svh w-full\",\n        brand ? \"bg-background lg:grid-cols-2\" : \"bg-muted\",\n        className,\n      )}\n      {...props}\n    >\n      <div\n        className={cn(\"flex flex-col gap-6 p-6 md:p-10\", !brand && \"items-center justify-center\")}\n      >\n        {logo && (\n          <div\n            data-slot=\"auth-logo\"\n            className={cn(\"flex\", brand ? \"justify-center md:justify-start\" : \"justify-center\")}\n          >\n            {logo}\n          </div>\n        )}\n        <main className={cn(\"flex w-full justify-center\", brand && \"flex-1 items-center\")}>\n          <div className=\"w-full max-w-sm\">{children}</div>\n        </main>\n      </div>\n      {brand && (\n        <aside\n          data-slot=\"auth-brand\"\n          data-surface={surface}\n          className={cn(\n            \"relative hidden overflow-hidden lg:flex lg:flex-col\",\n            surface === \"brand\" ? brandSurface : \"bg-muted\",\n          )}\n        >\n          {brand}\n        </aside>\n      )}\n    </div>\n  );\n}\n\n/**\n * The form's card: `title` is the page's `h1`, then the `description`, the\n * fields you put inside, and a centered `footer` for the links between screens\n * (\"No account yet? Sign up\").\n */\nfunction AuthCard({\n  title,\n  description,\n  footer,\n  className,\n  children,\n  ...props\n}: Omit<React.ComponentProps<typeof Card>, \"title\"> & {\n  title: React.ReactNode;\n  description?: React.ReactNode;\n  footer?: React.ReactNode;\n}) {\n  return (\n    <Card data-slot=\"auth-card\" className={cn(\"w-full gap-6 py-6\", className)} {...props}>\n      <CardHeader className=\"px-6 text-center\">\n        <h1 className=\"font-heading text-xl leading-snug font-semibold\">{title}</h1>\n        {description && <CardDescription>{description}</CardDescription>}\n      </CardHeader>\n      <CardContent className=\"flex flex-col gap-6 px-6\">\n        {children}\n        {footer && (\n          <div data-slot=\"auth-footer\" className=\"text-center text-sm text-muted-foreground\">\n            {footer}\n          </div>\n        )}\n      </CardContent>\n    </Card>\n  );\n}\n\n/** The form's main action: full width, with a spinner while `isPending`. */\nfunction AuthSubmit({\n  isPending,\n  className,\n  children,\n  ...props\n}: Omit<React.ComponentProps<typeof Button>, \"children\"> & { children?: React.ReactNode }) {\n  return (\n    <Button\n      type=\"submit\"\n      size=\"lg\"\n      isPending={isPending}\n      className={cn(\"w-full\", className)}\n      {...props}\n    >\n      {isPending && <Spinner data-icon=\"inline-start\" aria-hidden=\"true\" />}\n      {children}\n    </Button>\n  );\n}\n\ntype ResendCodeLabels = {\n  resend: string;\n  /** The disabled button while the cooldown runs, given the time left as `m:ss`. */\n  resendIn: (time: string) => string;\n};\n\ntype ResendCodeI18n = { labels?: Partial<ResendCodeLabels> };\n\nconst defaultResendCodeLabels: ResendCodeLabels = {\n  resend: \"Resend code\",\n  resendIn: (time) => `Resend code in ${time}`,\n};\n\nconst formatCooldown = (seconds: number) =>\n  `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, \"0\")}`;\n\n/**\n * \"Resend code\" behind a cooldown: disabled with a countdown, then enabled;\n * pressing it runs `onResend` (pending while its promise runs) and starts the\n * cooldown again. The cooldown starts on mount, since a code was just sent.\n */\nfunction ResendCode({\n  onResend,\n  cooldown = 60,\n  i18n,\n  className,\n  ...props\n}: Omit<React.ComponentProps<typeof Button>, \"onPress\" | \"children\"> & {\n  onResend: () => void | Promise<void>;\n  /** Seconds between sends. */\n  cooldown?: number;\n  i18n?: ResendCodeI18n;\n}) {\n  const labels = { ...defaultResendCodeLabels, ...i18n?.labels };\n  const [readyAt, setReadyAt] = React.useState(() => Date.now() + cooldown * 1000);\n  const [now, setNow] = React.useState(() => Date.now());\n  const [isPending, setIsPending] = React.useState(false);\n  const left = Math.max(0, Math.ceil((readyAt - now) / 1000));\n\n  React.useEffect(() => {\n    if (left === 0) return;\n    const timer = setInterval(() => setNow(Date.now()), 1000);\n    return () => clearInterval(timer);\n  }, [left]);\n\n  async function resend() {\n    setIsPending(true);\n    try {\n      await onResend();\n      const sentAt = Date.now();\n      setNow(sentAt);\n      setReadyAt(sentAt + cooldown * 1000);\n    } finally {\n      setIsPending(false);\n    }\n  }\n\n  return (\n    <Button\n      data-slot=\"resend-code\"\n      variant=\"link\"\n      size=\"sm\"\n      isDisabled={left > 0}\n      isPending={isPending}\n      onPress={resend}\n      className={cn(\"h-auto px-0 tabular-nums\", className)}\n      {...props}\n    >\n      {left > 0 ? labels.resendIn(formatCooldown(left)) : labels.resend}\n    </Button>\n  );\n}\n\nexport type { ResendCodeI18n, ResendCodeLabels };\nexport { AuthCard, AuthLayout, AuthSubmit, ResendCode };\n",
      "type": "registry:component",
      "target": "components/fma-ds/auth/auth.tsx"
    }
  ],
  "meta": {
    "status": "experimental",
    "source": "original",
    "license": "MIT",
    "tags": [
      "block",
      "auth",
      "login",
      "password",
      "otp"
    ],
    "contentHash": "c1859f7520ac"
  },
  "categories": [
    "blocks"
  ],
  "type": "registry:block"
}