{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "auth-magic-link",
  "title": "Auth Magic Link",
  "description": "Passwordless authentication via magic link email.",
  "registryDependencies": [
    "button",
    "card",
    "field",
    "input-group"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/auth/auth-magic-link.tsx",
      "content": "\"use client\";\n\nimport {\n  CheckCircle2,\n  Clock,\n  Loader2,\n  Mail,\n  RefreshCw,\n  XCircle,\n} from \"lucide-react\";\nimport { useCallback, useEffect, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/registry/new-york/ui/button\";\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/registry/new-york/ui/card\";\nimport {\n  Field,\n  FieldContent,\n  FieldDescription,\n  FieldError,\n  FieldLabel,\n} from \"@/registry/new-york/ui/field\";\nimport {\n  InputGroup,\n  InputGroupAddon,\n  InputGroupInput,\n} from \"@/registry/new-york/ui/input-group\";\n\nexport type MagicLinkStatus = \"pending\" | \"sent\" | \"verified\" | \"expired\";\n\nexport interface AuthMagicLinkProps {\n  onSubmit?: (email: string) => void;\n  onResend?: (email: string) => void;\n  className?: string;\n  defaultEmail?: string;\n  isLoading?: boolean;\n  status?: MagicLinkStatus;\n  email?: string;\n  resendCooldown?: number;\n  errors?: {\n    email?: string;\n    general?: string;\n  };\n  successMessage?: string;\n}\n\nfunction validateEmail(value: string): string | undefined {\n  if (!value.trim()) {\n    return \"Email is required\";\n  }\n  const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n  if (!emailRegex.test(value)) {\n    return \"Please enter a valid email address\";\n  }\n  return;\n}\n\ninterface ErrorAlertProps {\n  message: string;\n}\n\nfunction ErrorAlert({ message }: ErrorAlertProps) {\n  return (\n    <div\n      aria-live=\"polite\"\n      className=\"rounded-lg border border-destructive/50 bg-destructive/10 p-3 text-destructive text-sm\"\n      role=\"alert\"\n    >\n      {message}\n    </div>\n  );\n}\n\ninterface EmailFieldProps {\n  id: string;\n  value: string;\n  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;\n  error?: string;\n}\n\nfunction EmailField({ id, value, onChange, error }: EmailFieldProps) {\n  return (\n    <Field data-invalid={!!error}>\n      <FieldLabel htmlFor={id}>\n        Email\n        <span aria-label=\"required\" className=\"text-destructive\">\n          *\n        </span>\n      </FieldLabel>\n      <FieldContent>\n        <InputGroup aria-invalid={!!error}>\n          <InputGroupAddon>\n            <Mail aria-hidden=\"true\" className=\"size-4\" />\n          </InputGroupAddon>\n          <InputGroupInput\n            aria-describedby={error ? `${id}-error` : undefined}\n            aria-invalid={!!error}\n            autoComplete=\"email\"\n            id={id}\n            inputMode=\"email\"\n            name=\"email\"\n            onChange={onChange}\n            placeholder=\"name@example.com…\"\n            required\n            type=\"email\"\n            value={value}\n          />\n        </InputGroup>\n        {error && <FieldError id={`${id}-error`}>{error}</FieldError>}\n        <FieldDescription>\n          We&apos;ll send a secure link to sign in without a password\n        </FieldDescription>\n      </FieldContent>\n    </Field>\n  );\n}\n\ninterface ResendButtonProps {\n  cooldown: number;\n  isLoading: boolean;\n  onClick: () => void;\n  variant?: \"default\" | \"outline\";\n}\n\nfunction ResendButton({\n  cooldown,\n  isLoading,\n  onClick,\n  variant = \"default\",\n}: ResendButtonProps) {\n  return (\n    <Button\n      aria-busy={isLoading}\n      className=\"min-h-[44px] w-full touch-manipulation\"\n      data-loading={isLoading}\n      disabled={cooldown > 0 || isLoading}\n      onClick={onClick}\n      type=\"button\"\n      variant={variant}\n    >\n      {isLoading ? (\n        <>\n          <Loader2 aria-hidden=\"true\" className=\"size-4 animate-spin\" />\n          Sending…\n        </>\n      ) : cooldown > 0 ? (\n        <>\n          <Clock aria-hidden=\"true\" className=\"size-4\" />\n          Resend in {cooldown}s\n        </>\n      ) : (\n        <>\n          <RefreshCw aria-hidden=\"true\" className=\"size-4\" />\n          {variant === \"outline\"\n            ? \"Resend magic link\"\n            : \"Request new magic link\"}\n        </>\n      )}\n    </Button>\n  );\n}\n\ninterface VerifiedStateProps {\n  className?: string;\n}\n\nfunction VerifiedState({ className }: VerifiedStateProps) {\n  return (\n    <Card className={cn(\"w-full max-w-sm shadow-xs\", className)}>\n      <CardHeader>\n        <CardTitle>Email verified</CardTitle>\n        <CardDescription>You&apos;ve successfully signed in</CardDescription>\n      </CardHeader>\n      <CardContent>\n        <div className=\"flex flex-col items-center gap-4 rounded-lg border border-primary/20 bg-primary/5 p-6 text-center\">\n          <div className=\"flex size-16 items-center justify-center rounded-full bg-primary/10\">\n            <CheckCircle2 aria-hidden=\"true\" className=\"size-8 text-primary\" />\n          </div>\n          <div className=\"flex flex-col gap-2\">\n            <p className=\"font-medium text-sm\">\n              You&apos;re all set! Redirecting…\n            </p>\n          </div>\n        </div>\n      </CardContent>\n    </Card>\n  );\n}\n\ninterface ExpiredStateProps {\n  className?: string;\n  cooldown: number;\n  isLoading: boolean;\n  onResend?: () => void;\n}\n\nfunction ExpiredState({\n  className,\n  cooldown,\n  isLoading,\n  onResend,\n}: ExpiredStateProps) {\n  return (\n    <Card className={cn(\"w-full max-w-sm shadow-xs\", className)}>\n      <CardHeader>\n        <CardTitle>Link expired</CardTitle>\n        <CardDescription>\n          This magic link has expired. Please request a new one.\n        </CardDescription>\n      </CardHeader>\n      <CardContent>\n        <div className=\"flex flex-col gap-4\">\n          <div className=\"flex flex-col items-center gap-4 rounded-lg border border-destructive/50 bg-destructive/10 p-6 text-center\">\n            <div className=\"flex size-16 items-center justify-center rounded-full bg-destructive/10\">\n              <XCircle aria-hidden=\"true\" className=\"size-8 text-destructive\" />\n            </div>\n            <p className=\"text-destructive text-sm\">\n              The magic link has expired. Please request a new one.\n            </p>\n          </div>\n\n          {onResend && (\n            <ResendButton\n              cooldown={cooldown}\n              isLoading={isLoading}\n              onClick={onResend}\n            />\n          )}\n        </div>\n      </CardContent>\n    </Card>\n  );\n}\n\ninterface SentStateProps {\n  className?: string;\n  cooldown: number;\n  displayEmail?: string;\n  isLoading: boolean;\n  message: string;\n  onResend?: () => void;\n}\n\nfunction SentState({\n  className,\n  cooldown,\n  displayEmail,\n  isLoading,\n  message,\n  onResend,\n}: SentStateProps) {\n  return (\n    <Card className={cn(\"w-full max-w-sm shadow-xs\", className)}>\n      <CardHeader>\n        <CardTitle>Check your email</CardTitle>\n        <CardDescription>We&apos;ve sent you a magic link</CardDescription>\n      </CardHeader>\n      <CardContent>\n        <div className=\"flex flex-col gap-6\">\n          <div className=\"flex flex-col items-center gap-4 rounded-lg border border-primary/20 bg-primary/5 p-6 text-center\">\n            <div className=\"flex size-16 items-center justify-center rounded-full bg-primary/10\">\n              <Mail aria-hidden=\"true\" className=\"size-8 text-primary\" />\n            </div>\n            <div className=\"flex flex-col gap-2\">\n              <p className=\"font-medium text-sm\">{message}</p>\n              {displayEmail && (\n                <p className=\"text-muted-foreground text-sm\">\n                  Sent to <span className=\"font-medium\">{displayEmail}</span>\n                </p>\n              )}\n              <p className=\"text-muted-foreground text-xs\">\n                Click the link in the email to sign in. If you don&apos;t see\n                it, check your spam folder.\n              </p>\n            </div>\n          </div>\n\n          {onResend && (\n            <ResendButton\n              cooldown={cooldown}\n              isLoading={isLoading}\n              onClick={onResend}\n              variant=\"outline\"\n            />\n          )}\n        </div>\n      </CardContent>\n    </Card>\n  );\n}\n\nexport default function AuthMagicLink({\n  onSubmit,\n  onResend,\n  className,\n  defaultEmail = \"\",\n  isLoading = false,\n  status = \"pending\",\n  email: statusEmail,\n  resendCooldown = 60,\n  errors,\n  successMessage = \"We've sent a magic link to your email address.\",\n}: AuthMagicLinkProps) {\n  const [email, setEmail] = useState(defaultEmail);\n  const [cooldown, setCooldown] = useState(0);\n  const [localErrors, setLocalErrors] = useState<{\n    email?: string;\n  }>({});\n\n  useEffect(() => {\n    if (cooldown > 0) {\n      const timer = setTimeout(() => {\n        setCooldown((prev) => prev - 1);\n      }, 1000);\n      return () => clearTimeout(timer);\n    }\n  }, [cooldown]);\n\n  const handleSubmit = useCallback(\n    (e: React.FormEvent) => {\n      e.preventDefault();\n\n      const emailError = validateEmail(email);\n\n      if (emailError) {\n        setLocalErrors({ email: emailError });\n        return;\n      }\n\n      setLocalErrors({});\n      onSubmit?.(email.trim());\n    },\n    [email, onSubmit]\n  );\n\n  const handleResend = useCallback(async () => {\n    if (cooldown > 0) return;\n    const emailToUse = statusEmail || email;\n    if (emailToUse) {\n      await onResend?.(emailToUse);\n      setCooldown(resendCooldown);\n    }\n  }, [cooldown, onResend, statusEmail, email, resendCooldown]);\n\n  const handleEmailChange = useCallback(\n    (e: React.ChangeEvent<HTMLInputElement>) => {\n      const value = e.target.value;\n      setEmail(value);\n      if (localErrors.email) {\n        setLocalErrors((prev) => ({ ...prev, email: validateEmail(value) }));\n      }\n    },\n    [localErrors.email]\n  );\n\n  const emailError = errors?.email || localErrors.email;\n  const generalError = errors?.general;\n  const displayEmail = statusEmail || email;\n\n  if (status === \"verified\") {\n    return <VerifiedState className={className} />;\n  }\n\n  if (status === \"expired\") {\n    return (\n      <ExpiredState\n        className={className}\n        cooldown={cooldown}\n        isLoading={isLoading}\n        onResend={onResend ? handleResend : undefined}\n      />\n    );\n  }\n\n  if (status === \"sent\") {\n    return (\n      <SentState\n        className={className}\n        cooldown={cooldown}\n        displayEmail={displayEmail}\n        isLoading={isLoading}\n        message={successMessage}\n        onResend={onResend ? handleResend : undefined}\n      />\n    );\n  }\n\n  return (\n    <Card className={cn(\"w-full max-w-sm shadow-xs\", className)}>\n      <CardHeader>\n        <CardTitle>Sign in with magic link</CardTitle>\n        <CardDescription>\n          Enter your email and we&apos;ll send you a passwordless sign-in link\n        </CardDescription>\n      </CardHeader>\n      <CardContent>\n        <form className=\"flex flex-col gap-6\" onSubmit={handleSubmit}>\n          {generalError && <ErrorAlert message={generalError} />}\n\n          <EmailField\n            error={emailError}\n            id=\"magic-link-email\"\n            onChange={handleEmailChange}\n            value={email}\n          />\n\n          <Button\n            aria-busy={isLoading}\n            className=\"min-h-[44px] w-full touch-manipulation\"\n            data-loading={isLoading}\n            disabled={isLoading}\n            type=\"submit\"\n          >\n            {isLoading ? (\n              <>\n                <Loader2 aria-hidden=\"true\" className=\"size-4 animate-spin\" />\n                Sending magic link…\n              </>\n            ) : (\n              <>\n                <Mail aria-hidden=\"true\" className=\"size-4\" />\n                Send magic link\n              </>\n            )}\n          </Button>\n        </form>\n      </CardContent>\n    </Card>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "block",
    "auth"
  ],
  "type": "registry:ui"
}