{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "auth-signup-form",
  "title": "Auth Signup Form",
  "description": "User registration form with email/password and social signup options.",
  "registryDependencies": [
    "button",
    "card",
    "checkbox",
    "field",
    "input-group",
    "separator"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/auth/auth-signup-form.tsx",
      "content": "\"use client\";\n\nimport { IconBrandGithub } from \"@tabler/icons-react\";\nimport { Eye, EyeOff, Loader2, Lock, Mail, User } from \"lucide-react\";\nimport { useCallback, 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 { Checkbox } from \"@/registry/new-york/ui/checkbox\";\nimport {\n  Field,\n  FieldContent,\n  FieldDescription,\n  FieldError,\n  FieldLabel,\n} from \"@/registry/new-york/ui/field\";\nimport {\n  InputGroup,\n  InputGroupAddon,\n  InputGroupButton,\n  InputGroupInput,\n} from \"@/registry/new-york/ui/input-group\";\nimport { Separator } from \"@/registry/new-york/ui/separator\";\n\nexport interface SocialProvider {\n  id: string;\n  name: string;\n  icon: React.ComponentType<{ className?: string }>;\n}\n\nexport interface AuthSignupFormProps {\n  onSubmit?: (data: {\n    name: string;\n    email: string;\n    password: string;\n    confirmPassword: string;\n    acceptTerms: boolean;\n  }) => void;\n  onSocialLogin?: (provider: string) => void;\n  socialProviders?: SocialProvider[];\n  showSocialLogin?: boolean;\n  className?: string;\n  isLoading?: boolean;\n  errors?: {\n    name?: string;\n    email?: string;\n    password?: string;\n    confirmPassword?: string;\n    terms?: string;\n    general?: string;\n  };\n  termsUrl?: string;\n  privacyUrl?: string;\n}\n\nconst GoogleIcon = ({ className }: { className?: string }) => (\n  <svg\n    className={className}\n    viewBox=\"0 0 24 24\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <path\n      d=\"M12 2a9.96 9.96 0 0 1 6.29 2.226a1 1 0 0 1 .04 1.52l-1.51 1.362a1 1 0 0 1-1.265.06a6 6 0 1 0 2.103 6.836l.001-.004h-3.66a1 1 0 0 1-.992-.883L13 13v-2a1 1 0 0 1 1-1h6.945a1 1 0 0 1 .994.89q.06.55.061 1.11c0 5.523-4.477 10-10 10S2 17.523 2 12S6.477 2 12 2\"\n      fill=\"currentColor\"\n    />\n  </svg>\n);\n\nconst DEFAULT_SOCIAL_PROVIDERS: SocialProvider[] = [\n  { id: \"google\", name: \"Google\", icon: GoogleIcon },\n  { id: \"github\", name: \"GitHub\", icon: IconBrandGithub },\n];\n\nfunction validateName(value: string): string | undefined {\n  if (!value.trim()) {\n    return \"Name is required\";\n  }\n  if (value.trim().length < 2) {\n    return \"Name must be at least 2 characters\";\n  }\n  return;\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\nfunction validatePassword(value: string): string | undefined {\n  if (!value) {\n    return \"Password is required\";\n  }\n  if (value.length < 8) {\n    return \"Password must be at least 8 characters\";\n  }\n  if (!/(?=.*[a-z])/.test(value)) {\n    return \"Password must contain at least one lowercase letter\";\n  }\n  if (!/(?=.*[A-Z])/.test(value)) {\n    return \"Password must contain at least one uppercase letter\";\n  }\n  if (!/(?=.*\\d)/.test(value)) {\n    return \"Password must contain at least one number\";\n  }\n  return;\n}\n\nfunction validateConfirmPassword(\n  value: string,\n  passwordValue: string\n): string | undefined {\n  if (!value) {\n    return \"Please confirm your password\";\n  }\n  if (value !== passwordValue) {\n    return \"Passwords do not match\";\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 SocialLoginButtonsProps {\n  onSocialLogin?: (provider: string) => void;\n  providers: SocialProvider[];\n}\n\nfunction SocialLoginButtons({\n  providers,\n  onSocialLogin,\n}: SocialLoginButtonsProps) {\n  return (\n    <div className=\"flex flex-col gap-2\">\n      {providers.map((provider) => {\n        const Icon = provider.icon;\n        return (\n          <Button\n            aria-label={`Sign up with ${provider.name}`}\n            className=\"min-h-[44px] w-full touch-manipulation\"\n            key={provider.id}\n            onClick={(e) => {\n              e.preventDefault();\n              onSocialLogin?.(provider.id);\n            }}\n            type=\"button\"\n            variant=\"outline\"\n          >\n            <Icon aria-hidden=\"true\" className=\"size-4\" />\n            Continue with {provider.name}\n          </Button>\n        );\n      })}\n    </div>\n  );\n}\n\nfunction EmailSeparator() {\n  return (\n    <div className=\"relative\">\n      <Separator />\n      <div className=\"absolute inset-0 flex items-center justify-center\">\n        <span className=\"bg-card px-2 text-muted-foreground text-xs\">\n          Or continue with email\n        </span>\n      </div>\n    </div>\n  );\n}\n\ninterface NameFieldProps {\n  error?: string;\n  id: string;\n  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;\n  value: string;\n}\n\nfunction NameField({ id, value, onChange, error }: NameFieldProps) {\n  return (\n    <Field data-invalid={!!error}>\n      <FieldLabel htmlFor={id}>\n        Full name\n        <span aria-label=\"required\" className=\"text-destructive\">\n          *\n        </span>\n      </FieldLabel>\n      <FieldContent>\n        <InputGroup aria-invalid={!!error}>\n          <InputGroupAddon>\n            <User aria-hidden=\"true\" className=\"size-4\" />\n          </InputGroupAddon>\n          <InputGroupInput\n            aria-describedby={error ? `${id}-error` : undefined}\n            aria-invalid={!!error}\n            autoComplete=\"name\"\n            id={id}\n            name=\"name\"\n            onChange={onChange}\n            placeholder=\"John Doe…\"\n            required\n            type=\"text\"\n            value={value}\n          />\n        </InputGroup>\n        {error && <FieldError id={`${id}-error`}>{error}</FieldError>}\n      </FieldContent>\n    </Field>\n  );\n}\n\ninterface EmailFieldProps {\n  error?: string;\n  id: string;\n  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;\n  value: 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      </FieldContent>\n    </Field>\n  );\n}\n\ninterface PasswordFieldProps {\n  error?: string;\n  id: string;\n  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;\n  onTogglePassword: () => void;\n  showPassword: boolean;\n  value: string;\n}\n\nfunction PasswordField({\n  id,\n  value,\n  onChange,\n  showPassword,\n  onTogglePassword,\n  error,\n}: PasswordFieldProps) {\n  return (\n    <Field data-invalid={!!error}>\n      <FieldLabel htmlFor={id}>\n        Password\n        <span aria-label=\"required\" className=\"text-destructive\">\n          *\n        </span>\n      </FieldLabel>\n      <FieldContent>\n        <InputGroup aria-invalid={!!error}>\n          <InputGroupAddon>\n            <Lock aria-hidden=\"true\" className=\"size-4\" />\n          </InputGroupAddon>\n          <InputGroupInput\n            aria-describedby={error ? `${id}-error` : undefined}\n            aria-invalid={!!error}\n            autoComplete=\"new-password\"\n            id={id}\n            name=\"password\"\n            onChange={onChange}\n            placeholder=\"Create a password…\"\n            required\n            type={showPassword ? \"text\" : \"password\"}\n            value={value}\n          />\n          <InputGroupButton\n            aria-label={showPassword ? \"Hide password\" : \"Show password\"}\n            className=\"min-h-[32px] min-w-[32px] touch-manipulation\"\n            onClick={(e) => {\n              e.preventDefault();\n              onTogglePassword();\n            }}\n            type=\"button\"\n          >\n            {showPassword ? (\n              <EyeOff aria-hidden=\"true\" className=\"size-4\" />\n            ) : (\n              <Eye aria-hidden=\"true\" className=\"size-4\" />\n            )}\n          </InputGroupButton>\n        </InputGroup>\n        {error && <FieldError id={`${id}-error`}>{error}</FieldError>}\n        <FieldDescription>\n          Must be at least 8 characters with uppercase, lowercase, and number\n        </FieldDescription>\n      </FieldContent>\n    </Field>\n  );\n}\n\ninterface ConfirmPasswordFieldProps {\n  error?: string;\n  id: string;\n  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;\n  onTogglePassword: () => void;\n  showPassword: boolean;\n  value: string;\n}\n\nfunction ConfirmPasswordField({\n  id,\n  value,\n  onChange,\n  showPassword,\n  onTogglePassword,\n  error,\n}: ConfirmPasswordFieldProps) {\n  return (\n    <Field data-invalid={!!error}>\n      <FieldLabel htmlFor={id}>\n        Confirm password\n        <span aria-label=\"required\" className=\"text-destructive\">\n          *\n        </span>\n      </FieldLabel>\n      <FieldContent>\n        <InputGroup aria-invalid={!!error}>\n          <InputGroupAddon>\n            <Lock aria-hidden=\"true\" className=\"size-4\" />\n          </InputGroupAddon>\n          <InputGroupInput\n            aria-describedby={error ? `${id}-error` : undefined}\n            aria-invalid={!!error}\n            autoComplete=\"new-password\"\n            id={id}\n            name=\"confirmPassword\"\n            onChange={onChange}\n            placeholder=\"Confirm your password…\"\n            required\n            type={showPassword ? \"text\" : \"password\"}\n            value={value}\n          />\n          <InputGroupButton\n            aria-label={\n              showPassword ? \"Hide confirm password\" : \"Show confirm password\"\n            }\n            className=\"min-h-[32px] min-w-[32px] touch-manipulation\"\n            onClick={(e) => {\n              e.preventDefault();\n              onTogglePassword();\n            }}\n            type=\"button\"\n          >\n            {showPassword ? (\n              <EyeOff aria-hidden=\"true\" className=\"size-4\" />\n            ) : (\n              <Eye aria-hidden=\"true\" className=\"size-4\" />\n            )}\n          </InputGroupButton>\n        </InputGroup>\n        {error && <FieldError id={`${id}-error`}>{error}</FieldError>}\n      </FieldContent>\n    </Field>\n  );\n}\n\ninterface TermsCheckboxProps {\n  acceptTerms: boolean;\n  error?: string;\n  onAcceptTermsChange: (checked: boolean) => void;\n  privacyUrl: string;\n  termsUrl: string;\n}\n\nfunction TermsCheckbox({\n  acceptTerms,\n  error,\n  onAcceptTermsChange,\n  privacyUrl,\n  termsUrl,\n}: TermsCheckboxProps) {\n  return (\n    <Field data-invalid={!!error}>\n      <FieldContent className=\"flex flex-col\">\n        <div className=\"flex items-start gap-2\">\n          <Checkbox\n            checked={acceptTerms}\n            id=\"accept-terms\"\n            onCheckedChange={(checked) => onAcceptTermsChange(checked === true)}\n          />\n          <label\n            className=\"cursor-pointer text-sm leading-relaxed peer-disabled:cursor-not-allowed peer-disabled:opacity-70\"\n            htmlFor=\"accept-terms\"\n          >\n            I agree to the{\" \"}\n            <a\n              className=\"rounded-sm text-primary underline underline-offset-4 hover:text-primary/80 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2\"\n              href={termsUrl}\n              rel=\"noopener noreferrer\"\n              target=\"_blank\"\n            >\n              Terms of Service\n            </a>{\" \"}\n            and{\" \"}\n            <a\n              className=\"rounded-sm text-primary underline underline-offset-4 hover:text-primary/80 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2\"\n              href={privacyUrl}\n              rel=\"noopener noreferrer\"\n              target=\"_blank\"\n            >\n              Privacy Policy\n            </a>\n            <span aria-label=\"required\" className=\"text-destructive\">\n              *\n            </span>\n          </label>\n        </div>\n        {error && <FieldError>{error}</FieldError>}\n      </FieldContent>\n    </Field>\n  );\n}\n\nexport default function AuthSignupForm({\n  onSubmit,\n  onSocialLogin,\n  socialProviders = DEFAULT_SOCIAL_PROVIDERS,\n  showSocialLogin = true,\n  className,\n  isLoading = false,\n  errors,\n  termsUrl = \"#\",\n  privacyUrl = \"#\",\n}: AuthSignupFormProps) {\n  const [name, setName] = useState(\"\");\n  const [email, setEmail] = useState(\"\");\n  const [password, setPassword] = useState(\"\");\n  const [confirmPassword, setConfirmPassword] = useState(\"\");\n  const [acceptTerms, setAcceptTerms] = useState(false);\n  const [showPassword, setShowPassword] = useState(false);\n  const [showConfirmPassword, setShowConfirmPassword] = useState(false);\n  const [localErrors, setLocalErrors] = useState<{\n    name?: string;\n    email?: string;\n    password?: string;\n    confirmPassword?: string;\n    terms?: string;\n  }>({});\n\n  const handleSubmit = useCallback(\n    (e: React.FormEvent) => {\n      e.preventDefault();\n\n      const nameError = validateName(name);\n      const emailError = validateEmail(email);\n      const passwordError = validatePassword(password);\n      const confirmPasswordError = validateConfirmPassword(\n        confirmPassword,\n        password\n      );\n      const termsError = acceptTerms ? undefined : \"You must accept the terms\";\n\n      if (\n        nameError ||\n        emailError ||\n        passwordError ||\n        confirmPasswordError ||\n        termsError\n      ) {\n        setLocalErrors({\n          name: nameError,\n          email: emailError,\n          password: passwordError,\n          confirmPassword: confirmPasswordError,\n          terms: termsError,\n        });\n        return;\n      }\n\n      setLocalErrors({});\n      onSubmit?.({\n        name: name.trim(),\n        email: email.trim(),\n        password,\n        confirmPassword,\n        acceptTerms,\n      });\n    },\n    [name, email, password, confirmPassword, acceptTerms, onSubmit]\n  );\n\n  const handleNameChange = useCallback(\n    (e: React.ChangeEvent<HTMLInputElement>) => {\n      const value = e.target.value;\n      setName(value);\n      if (localErrors.name) {\n        setLocalErrors((prev) => ({ ...prev, name: validateName(value) }));\n      }\n    },\n    [localErrors.name]\n  );\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 handlePasswordChange = useCallback(\n    (e: React.ChangeEvent<HTMLInputElement>) => {\n      const value = e.target.value;\n      setPassword(value);\n      if (localErrors.password) {\n        setLocalErrors((prev) => ({\n          ...prev,\n          password: validatePassword(value),\n        }));\n      }\n      if (localErrors.confirmPassword && confirmPassword) {\n        setLocalErrors((prev) => ({\n          ...prev,\n          confirmPassword: validateConfirmPassword(confirmPassword, value),\n        }));\n      }\n    },\n    [localErrors.password, localErrors.confirmPassword, confirmPassword]\n  );\n\n  const handleConfirmPasswordChange = useCallback(\n    (e: React.ChangeEvent<HTMLInputElement>) => {\n      const value = e.target.value;\n      setConfirmPassword(value);\n      if (localErrors.confirmPassword) {\n        setLocalErrors((prev) => ({\n          ...prev,\n          confirmPassword: validateConfirmPassword(value, password),\n        }));\n      }\n    },\n    [localErrors.confirmPassword, password]\n  );\n\n  const handleTogglePassword = useCallback(() => {\n    setShowPassword((prev) => !prev);\n  }, []);\n\n  const handleToggleConfirmPassword = useCallback(() => {\n    setShowConfirmPassword((prev) => !prev);\n  }, []);\n\n  const handleAcceptTermsChange = useCallback((checked: boolean) => {\n    setAcceptTerms(checked);\n  }, []);\n\n  const nameError = errors?.name || localErrors.name;\n  const emailError = errors?.email || localErrors.email;\n  const passwordError = errors?.password || localErrors.password;\n  const confirmPasswordError =\n    errors?.confirmPassword || localErrors.confirmPassword;\n  const termsError = errors?.terms || localErrors.terms;\n  const generalError = errors?.general;\n\n  return (\n    <Card className={cn(\"w-full max-w-sm shadow-xs\", className)}>\n      <CardHeader>\n        <CardTitle>Create an account</CardTitle>\n        <CardDescription>Enter your information to get started</CardDescription>\n      </CardHeader>\n      <CardContent>\n        <form className=\"flex flex-col gap-6\" onSubmit={handleSubmit}>\n          {generalError && <ErrorAlert message={generalError} />}\n\n          {showSocialLogin && socialProviders.length > 0 && (\n            <>\n              <SocialLoginButtons\n                onSocialLogin={onSocialLogin}\n                providers={socialProviders}\n              />\n              <EmailSeparator />\n            </>\n          )}\n\n          <div className=\"flex flex-col gap-4\">\n            <NameField\n              error={nameError}\n              id=\"signup-name\"\n              onChange={handleNameChange}\n              value={name}\n            />\n\n            <EmailField\n              error={emailError}\n              id=\"signup-email\"\n              onChange={handleEmailChange}\n              value={email}\n            />\n\n            <PasswordField\n              error={passwordError}\n              id=\"signup-password\"\n              onChange={handlePasswordChange}\n              onTogglePassword={handleTogglePassword}\n              showPassword={showPassword}\n              value={password}\n            />\n\n            <ConfirmPasswordField\n              error={confirmPasswordError}\n              id=\"signup-confirm-password\"\n              onChange={handleConfirmPasswordChange}\n              onTogglePassword={handleToggleConfirmPassword}\n              showPassword={showConfirmPassword}\n              value={confirmPassword}\n            />\n\n            <TermsCheckbox\n              acceptTerms={acceptTerms}\n              error={termsError}\n              onAcceptTermsChange={handleAcceptTermsChange}\n              privacyUrl={privacyUrl}\n              termsUrl={termsUrl}\n            />\n          </div>\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                Creating account…\n              </>\n            ) : (\n              \"Create account\"\n            )}\n          </Button>\n        </form>\n      </CardContent>\n    </Card>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "block",
    "auth"
  ],
  "type": "registry:ui"
}