{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "signup-form",
  "title": "Signup Form",
  "author": "ilinxa",
  "description": "Email and password signup with optional profile step, OAuth row, password strength meter, magic-link variant, consent gate, and honeypot.",
  "dependencies": [
    "react-hook-form@^7.75.0",
    "@hookform/resolvers@^5.2.2",
    "zod@^4.4.3",
    "lucide-react@^1.11.0"
  ],
  "registryDependencies": [
    "input",
    "label",
    "button",
    "checkbox"
  ],
  "files": [
    {
      "path": "src/registry/components/forms/signup-form/signup-form.tsx",
      "content": "\"use client\";\n\nimport { useId, useMemo, type ReactElement } from \"react\";\nimport { FormProvider } from \"react-hook-form\";\nimport { cn } from \"@/lib/utils\";\n\nimport { useSignupForm } from \"./hooks/use-signup-form\";\nimport { mergeSignupLabels } from \"./parts/labels\";\nimport { EmailField } from \"./parts/email-field\";\nimport { PasswordField } from \"./parts/password-field\";\nimport { ConsentCheckbox } from \"./parts/consent-checkbox\";\nimport { HoneypotField } from \"./parts/honeypot-field\";\nimport { OptionalFieldsFieldset } from \"./parts/optional-fields-fieldset\";\nimport { StepIndicator } from \"./parts/step-indicator\";\nimport { StepShell } from \"./parts/step-shell\";\nimport { ServerError } from \"./parts/server-error\";\nimport { SuccessScreen } from \"./parts/success-screen\";\nimport { SubmitRow } from \"./parts/submit-row\";\nimport { SignInLink } from \"./parts/sign-in-link\";\nimport { OauthRow } from \"./parts/oauth-row\";\nimport { StrengthMeter } from \"./parts/strength-meter\";\nimport type { SignupFormProps } from \"./types\";\n\nimport \"./signup-form.css\";\n\n/**\n * Schema-driven sign-up surface — hand-rolled on RHF v7 + zod v4. Two\n * flow variants (single-step / two-step) × two password strategies\n * (password / magic-link) × two densities (compact / default) × optional\n * OAuth row + declarative optional-fields bag + off-screen honeypot.\n *\n * See `docs/procomps/signup-form-procomp/` for the GATE 1 +\n * GATE 2 contract and `usage.tsx` (C6) for the consumer guide. The\n * load-bearing public-API surfaces are:\n *\n * 1. The **discriminated submit payload**: `onSubmit` receives one of\n *    three envelope shapes (`stepCompleted: \"single\" | \"step1\" | \"step2\"`).\n *    Consumers MUST switch on the discriminant. Narrowing optional\n *    profile fields as `| undefined` was rejected because it silently\n *    lets naïve destructuring send `undefined` values to backend APIs.\n *\n * 2. The **status mutual-exclusion contract**: if `status` is provided,\n *    internal state is read-only — the component reads `status` as\n *    source of truth and fires `onStatusChange` for observers only.\n *    Mixing the two (passing `status` AND expecting internal\n *    transitions) is a contract violation.\n *\n * 3. The **honeypot two-key duality**: HTML `name=\"website\"` (spam-bait)\n *    + RHF register key `_honeypot` (clean values shape). Off-screen\n *    rendering is load-bearing — `display: none` is bot-detectable.\n */\nexport function SignupForm(props: SignupFormProps): ReactElement {\n  const {\n    heading,\n    subheading,\n    headingAs: HeadingTag = \"h2\",\n    flow = \"single-step\",\n    passwordStrategy = \"password\",\n    density = \"default\",\n    skippableStepTwo = true,\n    fields = {},\n    passwordPolicy,\n    strengthCalculator,\n    consent,\n    oauthProviders = [],\n    oauthIcons,\n    onOAuthClick,\n    submitButton,\n    signInHref,\n    errorMessage,\n    successMessage,\n    status,\n    onStatusChange,\n    onSubmit,\n    labels: labelsOverride,\n    className,\n    style,\n  } = props;\n\n  const headingId = useId();\n  const labels = useMemo(\n    () => mergeSignupLabels(labelsOverride),\n    [labelsOverride],\n  );\n\n  const {\n    form,\n    status: effectiveStatus,\n    step,\n    goBack,\n    submit,\n    skip,\n  } = useSignupForm({\n    flow,\n    passwordStrategy,\n    skippableStepTwo,\n    fields,\n    passwordPolicy,\n    consent,\n    labels,\n    status,\n    onStatusChange,\n    onSubmit,\n  });\n\n  const showStrengthMeter =\n    passwordStrategy === \"password\" &&\n    (passwordPolicy?.showStrengthMeter ?? true);\n  const showPasswordField = passwordStrategy === \"password\";\n  const showOptionalFields =\n    flow === \"single-step\" ||\n    (flow === \"two-step\" && step === \"step2\");\n  const showStep1Fields =\n    flow === \"single-step\" ||\n    (flow === \"two-step\" && step === \"step1\");\n  const isSubmitting = effectiveStatus === \"submitting\";\n  const isSuccess = effectiveStatus === \"success\";\n  const submitLabel =\n    submitButton?.label ?? labels.createAccountLabel;\n\n  if (isSuccess) {\n    return (\n      <section\n        aria-labelledby={heading ? headingId : undefined}\n        className={cn(\n          \"flex w-full flex-col gap-4\",\n          density === \"compact\" ? \"max-w-sm\" : \"max-w-md\",\n          className,\n        )}\n        style={style}\n      >\n        {heading ? (\n          <HeadingTag\n            id={headingId}\n            className=\"text-xl font-semibold text-foreground\"\n          >\n            {heading}\n          </HeadingTag>\n        ) : null}\n        <SuccessScreen\n          message={successMessage ?? labels.successDefaultMessage}\n        />\n      </section>\n    );\n  }\n\n  return (\n    <section\n      aria-labelledby={heading ? headingId : undefined}\n      className={cn(\n        \"flex w-full flex-col\",\n        density === \"compact\" ? \"max-w-sm gap-3\" : \"max-w-md gap-4\",\n        className,\n      )}\n      style={style}\n    >\n      {heading ? (\n        <HeadingTag\n          id={headingId}\n          className={cn(\n            \"font-semibold text-foreground\",\n            density === \"compact\" ? \"text-lg\" : \"text-xl\",\n          )}\n        >\n          {heading}\n        </HeadingTag>\n      ) : null}\n      {subheading ? (\n        <p className=\"-mt-2 text-sm text-muted-foreground\">{subheading}</p>\n      ) : null}\n\n      {flow === \"two-step\" ? (\n        <StepIndicator\n          current={step === \"step1\" ? 1 : 2}\n          total={2}\n          labels={labels}\n        />\n      ) : null}\n\n      {errorMessage ? <ServerError message={errorMessage} /> : null}\n\n      <OauthRow\n        providers={oauthProviders}\n        icons={oauthIcons}\n        labels={labels}\n        disabled={isSubmitting}\n        onClick={(provider) => onOAuthClick?.({ provider })}\n      />\n\n      <FormProvider {...form}>\n        <form\n          noValidate\n          className={cn(\n            \"flex flex-col\",\n            density === \"compact\" ? \"gap-3\" : \"gap-4\",\n          )}\n          onSubmit={(e) => {\n            e.preventDefault();\n            void submit();\n          }}\n        >\n          <StepShell step={step}>\n            {showStep1Fields ? (\n              <>\n                <EmailField labels={labels} />\n                {showPasswordField ? (\n                  <PasswordField\n                    labels={labels}\n                    strengthMeter={\n                      showStrengthMeter ? (\n                        <StrengthMeter\n                          calculator={strengthCalculator}\n                          labels={labels}\n                        />\n                      ) : null\n                    }\n                  />\n                ) : null}\n                <ConsentCheckbox consent={consent} labels={labels} />\n              </>\n            ) : null}\n            {showOptionalFields ? (\n              <OptionalFieldsFieldset\n                fields={fields}\n                labels={labels}\n                density={density}\n              />\n            ) : null}\n          </StepShell>\n\n          <HoneypotField labels={labels} />\n\n          <SubmitRow\n            status={effectiveStatus}\n            flow={flow}\n            step={step}\n            skippableStepTwo={skippableStepTwo}\n            submitLabel={submitLabel}\n            submitVariant={submitButton?.variant}\n            labels={labels}\n            onBack={goBack}\n            onSkip={() => void skip()}\n          />\n        </form>\n      </FormProvider>\n\n      {signInHref ? (\n        <SignInLink href={signInHref} labels={labels} />\n      ) : null}\n    </section>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/signup-form/signup-form.tsx"
    },
    {
      "path": "src/registry/components/forms/signup-form/signup-form.css",
      "content": "/**\n * Sealed-folder CSS for the step-shell fade transition. Imported once\n * at the root component (`signup-form.tsx`). The fade collapses\n * to a hard 0ms swap under `prefers-reduced-motion: reduce` so a11y\n * tooling isn't disorientated.\n */\n\n@keyframes signup-form-step-fade-in {\n  from {\n    opacity: 0;\n    transform: translateY(2px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n[data-signup-form-step] {\n  animation: signup-form-step-fade-in 150ms ease-out both;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  [data-signup-form-step] {\n    animation: none;\n  }\n}\n",
      "type": "registry:component",
      "target": "components/signup-form/signup-form.css"
    },
    {
      "path": "src/registry/components/forms/signup-form/index.ts",
      "content": "// Component\nexport { SignupForm } from \"./signup-form\";\n\n// Public types\nexport type {\n  SignupFormProps,\n  SignupFormStatus,\n  SignupSubmitPayload,\n  SignupStep1Values,\n  SignupStep2Values,\n  SignupLabels,\n  OAuthProvider,\n  OptionalFieldName,\n  OptionalFieldConfig,\n  StrengthCalculator,\n  ButtonVariant,\n} from \"./types\";\n\n// Helpers\nexport {\n  defaultSignupLabels,\n  mergeSignupLabels,\n} from \"./parts/labels\";\nexport { defaultStrengthCalculator } from \"./lib/strength-calculator\";\nexport { OAUTH_PROVIDERS } from \"./lib/oauth-providers\";\n",
      "type": "registry:component",
      "target": "components/signup-form/index.ts"
    },
    {
      "path": "src/registry/components/forms/signup-form/types.ts",
      "content": "import type { CSSProperties, ReactNode } from \"react\";\n\n/**\n * Variant of the standard shadcn Button — matches `ButtonProps[\"variant\"]`\n * without taking a runtime dep on the primitive.\n */\nexport type ButtonVariant =\n  | \"default\"\n  | \"destructive\"\n  | \"outline\"\n  | \"secondary\"\n  | \"ghost\"\n  | \"link\";\n\n/** Built-in OAuth providers the signup form knows about. */\nexport type OAuthProvider =\n  | \"google\"\n  | \"github\"\n  | \"apple\"\n  | \"facebook\"\n  | \"microsoft\"\n  | \"x\";\n\n/** Optional profile fields the consumer can opt into via the `fields` prop. */\nexport type OptionalFieldName =\n  | \"firstName\"\n  | \"lastName\"\n  | \"displayName\"\n  | \"phone\"\n  | \"company\";\n\n/**\n * Lifecycle status. Internal state machine by default; mutable via the\n * controlled `status` / `onStatusChange` escape hatch.\n */\nexport type SignupFormStatus =\n  | \"idle\"\n  | \"submitting\"\n  | \"success\"\n  | \"error\";\n\n/**\n * Pluggable password-strength calculator. 0 = empty / unrated. 1–4 =\n * weak / fair / strong / excellent.\n *\n * v0.1 default is the built-in `(length, character-class-count)` heuristic\n * in `lib/strength-calculator.ts`. Consumers with corporate password\n * policies or who want to drop in zxcvbn replace this slot; v0.2 may ship\n * an opt-in zxcvbn adapter that satisfies the same shape.\n */\nexport type StrengthCalculator = (password: string) => 0 | 1 | 2 | 3 | 4;\n\n/**\n * Field-config entry. `true` = show + optional. `{ required: true }` =\n * show + required. `false` (or omitted key) = hidden.\n */\nexport type OptionalFieldConfig = boolean | { required: boolean };\n\n// ─── Values ──────────────────────────────────────────────────────────────────\n\n/**\n * Step-1 values. Email + (optional, magic-link drops it) password + consent\n * + honeypot. Always present at the bottom of the values stack regardless\n * of flow.\n *\n * **`password` is statically typed as optional** because the magic-link\n * strategy drops the field entirely. The Zod resolver enforces required\n * at runtime under `passwordStrategy: \"password\"`; the static type stays\n * permissive to keep one shape across both strategies.\n */\nexport interface SignupStep1Values {\n  email: string;\n  password?: string;\n  consentAccepted: boolean;\n  /**\n   * Honeypot field value. **Should always be `\"\"`.** A non-empty value\n   * means a spam bot filled the off-screen `<input name=\"website\">` —\n   * the discriminated payload flag `isHoneypotTripped` reflects this.\n   */\n  _honeypot: string;\n}\n\n/** Step-2 values: step-1 plus the optional profile bag. */\nexport interface SignupStep2Values extends SignupStep1Values {\n  firstName?: string;\n  lastName?: string;\n  displayName?: string;\n  phone?: string;\n  company?: string;\n}\n\n/**\n * Discriminated submit-payload envelope.\n *\n * Consumers MUST switch on `stepCompleted` to know whether the optional\n * profile fields are populated. Narrowing optional fields as\n * `string | undefined` was rejected because it silently lets naïve\n * destructuring (`const { firstName, lastName } = payload.values`) send\n * `undefined` values to backend APIs. The envelope forces the discriminant.\n *\n * - `\"single\"` — `flow: \"single-step\"`.\n * - `\"step1\"` — `flow: \"two-step\"` + user clicked \"Skip for now\".\n * - `\"step2\"` — `flow: \"two-step\"` + user completed step 2.\n */\nexport type SignupSubmitPayload =\n  | {\n      stepCompleted: \"single\";\n      values: SignupStep2Values;\n      isHoneypotTripped: boolean;\n    }\n  | {\n      stepCompleted: \"step1\";\n      values: SignupStep1Values;\n      isHoneypotTripped: boolean;\n    }\n  | {\n      stepCompleted: \"step2\";\n      values: SignupStep2Values;\n      isHoneypotTripped: boolean;\n    };\n\n// ─── Labels (i18n bag) ───────────────────────────────────────────────────────\n\nexport interface SignupLabels {\n  // Field labels + placeholders\n  emailLabel: string;\n  emailPlaceholder: string;\n  passwordLabel: string;\n  passwordPlaceholder: string;\n  firstNameLabel: string;\n  firstNamePlaceholder: string;\n  lastNameLabel: string;\n  lastNamePlaceholder: string;\n  displayNameLabel: string;\n  displayNamePlaceholder: string;\n  phoneLabel: string;\n  phonePlaceholder: string;\n  companyLabel: string;\n  companyPlaceholder: string;\n\n  // Buttons\n  createAccountLabel: string;\n  continueLabel: string;\n  backLabel: string;\n  skipForNowLabel: string;\n  signInLabel: string;\n\n  // OAuth\n  orContinueWith: string;\n  /** Template — `{provider}` interpolated with capitalized provider name. */\n  oauthLabelTemplate: string;\n\n  // Step indicator\n  /** Template — `{current}` and `{total}` interpolated. */\n  stepOf: string;\n\n  // Error messages\n  emailRequired: string;\n  emailInvalid: string;\n  passwordRequired: string;\n  /** Template — `{min}` interpolated with the minLength policy value. */\n  passwordTooShort: string;\n  passwordMissingUppercase: string;\n  passwordMissingNumber: string;\n  passwordMissingSymbol: string;\n  consentRequired: string;\n\n  // Strength meter\n  strengthWeak: string;\n  strengthFair: string;\n  strengthStrong: string;\n  strengthExcellent: string;\n  /** Template — `{level}` interpolated with the bucketed strength label. */\n  strengthLabel: string;\n\n  // Server-error fallback\n  serverError: string;\n\n  // Success\n  successDefaultMessage: string;\n\n  // sr-only accessibility hints\n  passwordToggleShow: string;\n  passwordToggleHide: string;\n  honeypotLabel: string;\n}\n\n// ─── Component props ─────────────────────────────────────────────────────────\n\nexport interface SignupFormProps {\n  /** Heading rendered above the form. Optional — consumer may render their own. */\n  heading?: string;\n  /** Subheading below the heading. */\n  subheading?: string;\n  /** Semantic level for the heading. Defaults to `h2`. */\n  headingAs?: \"h1\" | \"h2\" | \"h3\";\n\n  /** Flow shape. Default `\"single-step\"`. */\n  flow?: \"single-step\" | \"two-step\";\n  /** Password vs magic-link strategy. Default `\"password\"`. */\n  passwordStrategy?: \"password\" | \"magic-link\";\n  /** Vertical-rhythm density. Default `\"default\"`. */\n  density?: \"compact\" | \"default\";\n  /**\n   * Controls whether step 2 of the two-step flow renders a \"Skip for now\"\n   * button. Default `true`. Ignored when `flow === \"single-step\"`.\n   */\n  skippableStepTwo?: boolean;\n\n  /**\n   * Optional-fields bag. Each key:\n   * - `true` → show + optional\n   * - `false` (or omitted key) → hidden\n   * - `{ required: true }` → show + required\n   */\n  fields?: Partial<Record<OptionalFieldName, OptionalFieldConfig>>;\n\n  /**\n   * Password validators + strength-meter toggle. Ignored under\n   * `passwordStrategy: \"magic-link\"`.\n   */\n  passwordPolicy?: {\n    /** Default 8. */\n    minLength?: number;\n    requireUppercase?: boolean;\n    requireNumber?: boolean;\n    requireSymbol?: boolean;\n    /** Default `true`. */\n    showStrengthMeter?: boolean;\n  };\n\n  /**\n   * BYO strength heuristic — v0.1 contract for the v0.2 zxcvbn opt-in.\n   * Default is the built-in `(length, character-class-count)` scorer in\n   * `lib/strength-calculator.ts`.\n   */\n  strengthCalculator?: StrengthCalculator;\n\n  /**\n   * ToS consent gate. `required: true` blocks submit until checked.\n   * `label` accepts a `ReactNode` for inline `<Link>` composition;\n   * the `string + href?` convenience overload renders a plain anchor.\n   */\n  consent: {\n    required: boolean;\n    label: ReactNode | string;\n    /** Only used when `label` is a string. */\n    href?: string;\n  };\n\n  /**\n   * Render OAuth buttons above the email field. Empty array (or omitted)\n   * renders no OAuth row and no divider.\n   */\n  oauthProviders?: ReadonlyArray<OAuthProvider>;\n  /** Per-provider icon override slot. Default: text-only buttons. */\n  oauthIcons?: Partial<Record<OAuthProvider, ReactNode>>;\n  /**\n   * Fires on OAuth button click. The component does NOT handle the\n   * redirect / SDK call — consumer drives the actual OAuth handshake.\n   */\n  onOAuthClick?: (args: { provider: OAuthProvider }) => void;\n\n  /**\n   * Submit button label + variant override. Defaults:\n   * `{ label: \"Create account\", variant: \"default\" }`. On step 1 of the\n   * two-step flow, label auto-overrides to `\"Continue\"`.\n   */\n  submitButton?: { label?: string; variant?: ButtonVariant };\n\n  /**\n   * Renders \"Already have an account? Sign in\" link below the form when\n   * provided. Omit to hide the link.\n   */\n  signInHref?: string;\n\n  /**\n   * Controlled server-error message. When set, renders a `role=\"alert\"`\n   * banner above the form. Takes display precedence over the internal\n   * `error` status fallback.\n   */\n  errorMessage?: string;\n  /**\n   * Success-screen content. Renders on `onSubmit` resolving without\n   * throwing (default), or on `status === \"success\"` (controlled).\n   */\n  successMessage?: ReactNode | string;\n\n  /**\n   * Controlled status escape hatch. **Mutual exclusion:** if provided,\n   * internal state becomes read-only — the component reads `status` as\n   * source of truth and fires `onStatusChange` for observers only. If\n   * omitted, internal state owns transitions and `onStatusChange` fires\n   * on each one. Mixing the two (passing `status` AND expecting internal\n   * transitions) is a contract violation.\n   */\n  status?: SignupFormStatus;\n  /** Fires on every status transition (internal or controlled-mode echo). */\n  onStatusChange?: (status: SignupFormStatus) => void;\n\n  /** Submit handler — receives the discriminated envelope. */\n  onSubmit: (payload: SignupSubmitPayload) => void | Promise<void>;\n\n  /** Per-string overrides. Falls back to `defaultSignupLabels`. */\n  labels?: Partial<SignupLabels>;\n\n  /** Container className — applied to the outermost wrapper. */\n  className?: string;\n  /** Container style — applied to the outermost wrapper. */\n  style?: CSSProperties;\n}\n",
      "type": "registry:component",
      "target": "components/signup-form/types.ts"
    },
    {
      "path": "src/registry/components/forms/signup-form/parts/email-field.tsx",
      "content": "\"use client\";\n\nimport { useId } from \"react\";\nimport { useFormContext } from \"react-hook-form\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport type {\n  SignupLabels,\n  SignupStep2Values,\n} from \"../types\";\n\nexport interface EmailFieldProps {\n  labels: Pick<SignupLabels, \"emailLabel\" | \"emailPlaceholder\">;\n}\n\n/**\n * Email input + label + inline error. ARIA: `aria-describedby` points\n * to the inline-error region; the region carries `role=\"alert\"` only\n * when populated.\n */\nexport function EmailField({ labels }: EmailFieldProps) {\n  const id = useId();\n  const errorId = `${id}-error`;\n  const {\n    register,\n    formState: { errors },\n  } = useFormContext<SignupStep2Values>();\n  const error = errors.email?.message;\n\n  return (\n    <div className=\"flex flex-col gap-1.5\">\n      <Label htmlFor={id}>{labels.emailLabel}</Label>\n      <Input\n        id={id}\n        type=\"email\"\n        autoComplete=\"email\"\n        placeholder={labels.emailPlaceholder}\n        aria-invalid={error ? true : undefined}\n        aria-describedby={error ? errorId : undefined}\n        {...register(\"email\")}\n      />\n      <p\n        id={errorId}\n        role={error ? \"alert\" : undefined}\n        className={cn(\n          \"min-h-5 text-xs\",\n          error ? \"text-destructive\" : \"text-transparent\",\n        )}\n      >\n        {error ?? \"\"}\n      </p>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/signup-form/parts/email-field.tsx"
    },
    {
      "path": "src/registry/components/forms/signup-form/parts/password-field.tsx",
      "content": "\"use client\";\n\nimport { useId, useState, type ReactNode } from \"react\";\nimport { useFormContext } from \"react-hook-form\";\nimport { Eye, EyeOff } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport type {\n  SignupLabels,\n  SignupStep2Values,\n} from \"../types\";\n\nexport interface PasswordFieldProps {\n  labels: Pick<\n    SignupLabels,\n    | \"passwordLabel\"\n    | \"passwordPlaceholder\"\n    | \"passwordToggleShow\"\n    | \"passwordToggleHide\"\n  >;\n  /** Optional strength meter slot — passed in from the root component. */\n  strengthMeter?: ReactNode;\n}\n\n/**\n * Password input + label + inline error + show/hide eye toggle.\n *\n * The toggle is local state (uncontrolled — irrelevant to the form\n * values). `aria-pressed` + `aria-label` are surfaced for SR users so\n * the button state is announced. The strength meter is passed in as a\n * slot from the root component so the password value sub doesn't have\n * to thread the calculator + labels here.\n */\nexport function PasswordField({\n  labels,\n  strengthMeter,\n}: PasswordFieldProps) {\n  const id = useId();\n  const errorId = `${id}-error`;\n  const [shown, setShown] = useState(false);\n  const {\n    register,\n    formState: { errors },\n  } = useFormContext<SignupStep2Values>();\n  const error = errors.password?.message;\n\n  return (\n    <div className=\"flex flex-col gap-1.5\">\n      <Label htmlFor={id}>{labels.passwordLabel}</Label>\n      <div className=\"relative\">\n        <Input\n          id={id}\n          type={shown ? \"text\" : \"password\"}\n          autoComplete=\"new-password\"\n          placeholder={labels.passwordPlaceholder}\n          aria-invalid={error ? true : undefined}\n          aria-describedby={error ? errorId : undefined}\n          className=\"pe-10\"\n          {...register(\"password\")}\n        />\n        <Button\n          type=\"button\"\n          variant=\"ghost\"\n          size=\"icon\"\n          className=\"absolute inset-e-1 top-1/2 -translate-y-1/2 size-7\"\n          aria-pressed={shown}\n          aria-label={shown ? labels.passwordToggleHide : labels.passwordToggleShow}\n          onClick={() => setShown((v) => !v)}\n        >\n          {shown ? <EyeOff /> : <Eye />}\n        </Button>\n      </div>\n      {strengthMeter}\n      <p\n        id={errorId}\n        role={error ? \"alert\" : undefined}\n        className={cn(\n          \"min-h-5 text-xs\",\n          error ? \"text-destructive\" : \"text-transparent\",\n        )}\n      >\n        {error ?? \"\"}\n      </p>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/signup-form/parts/password-field.tsx"
    },
    {
      "path": "src/registry/components/forms/signup-form/parts/consent-checkbox.tsx",
      "content": "\"use client\";\n\nimport { useId, type ReactNode } from \"react\";\nimport { Controller, useFormContext } from \"react-hook-form\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport type {\n  SignupLabels,\n  SignupStep2Values,\n} from \"../types\";\n\nexport interface ConsentCheckboxProps {\n  consent: {\n    required: boolean;\n    label: ReactNode | string;\n    href?: string;\n  };\n  labels: Pick<SignupLabels, \"consentRequired\">;\n}\n\n/**\n * Consent checkbox + label. Wired via `<Controller>` because the\n * underlying Radix Checkbox is controlled via `checked` / `onCheckedChange`\n * rather than the native `<input type=\"checkbox\">` interface.\n *\n * **F-cross-13 defensive callback shape:** `onCheckedChange` receives\n * `boolean | \"indeterminate\"` (Radix) or `boolean` (Base UI). The\n * coercion `typeof v === \"boolean\" ? v : true` widens both backends.\n *\n * Label accepts `ReactNode` for inline `<Link>` composition; the\n * `string + href?` convenience overload renders a plain `<a>`.\n */\nexport function ConsentCheckbox({\n  consent,\n  labels,\n}: ConsentCheckboxProps) {\n  const id = useId();\n  const errorId = `${id}-error`;\n  const {\n    control,\n    formState: { errors },\n  } = useFormContext<SignupStep2Values>();\n  const error = errors.consentAccepted?.message;\n\n  const labelNode: ReactNode =\n    typeof consent.label === \"string\" ? (\n      consent.href ? (\n        <a\n          href={consent.href}\n          className=\"underline underline-offset-2 hover:text-foreground\"\n          rel=\"noreferrer\"\n        >\n          {consent.label}\n        </a>\n      ) : (\n        consent.label\n      )\n    ) : (\n      consent.label\n    );\n\n  return (\n    <div className=\"flex flex-col gap-1.5\">\n      <div className=\"flex items-start gap-2\">\n        <Controller\n          control={control}\n          name=\"consentAccepted\"\n          render={({ field }) => (\n            <Checkbox\n              id={id}\n              checked={!!field.value}\n              onCheckedChange={(v) =>\n                field.onChange(typeof v === \"boolean\" ? v : true)\n              }\n              onBlur={field.onBlur}\n              aria-invalid={error ? true : undefined}\n              aria-describedby={error ? errorId : undefined}\n              aria-required={consent.required ? true : undefined}\n            />\n          )}\n        />\n        <Label\n          htmlFor={id}\n          className=\"text-sm font-normal leading-snug text-muted-foreground\"\n        >\n          {labelNode}\n        </Label>\n      </div>\n      <p\n        id={errorId}\n        role={error ? \"alert\" : undefined}\n        className={cn(\n          \"min-h-5 text-xs\",\n          error ? \"text-destructive\" : \"text-transparent\",\n        )}\n      >\n        {/* Always render the message text for layout stability; opacity\n            collapses to transparent when no error so users don't see a\n            premature \"you must accept\" warning at idle. */}\n        {error ?? labels.consentRequired}\n      </p>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/signup-form/parts/consent-checkbox.tsx"
    },
    {
      "path": "src/registry/components/forms/signup-form/parts/honeypot-field.tsx",
      "content": "\"use client\";\n\nimport { useFormContext } from \"react-hook-form\";\nimport {\n  HONEYPOT_HTML_NAME,\n  HONEYPOT_RHF_KEY,\n  HONEYPOT_STYLE,\n} from \"../lib/honeypot\";\nimport type { SignupLabels, SignupStep2Values } from \"../types\";\n\nexport interface HoneypotFieldProps {\n  labels: Pick<SignupLabels, \"honeypotLabel\">;\n}\n\n/**\n * Off-screen anti-spam honeypot. **Two non-obvious facts** worth keeping\n * in mind during refactors:\n *\n * 1. The `style` prop uses absolute off-screen position rather than\n *    `display: none`. `display: none` is bot-detectable; serious form-fill\n *    bots skip non-visible fields. The off-screen pattern keeps the field\n *    *rendered* (and fillable by naïve bots) while invisible to real users.\n *\n * 2. **Attribute order matters.** `{...register(HONEYPOT_RHF_KEY)}`\n *    returns `{ name: \"_honeypot\", onChange, onBlur, ref }`. The\n *    `name={HONEYPOT_HTML_NAME}` override MUST come AFTER the spread,\n *    otherwise React drops the override and the rendered HTML reads\n *    `name=\"_honeypot\"` — defeating the spam-bait. The two-key duality\n *    (HTML name \"website\" vs RHF key \"_honeypot\") is intentional:\n *    consumers see `payload.values._honeypot` cleanly without a confusing\n *    `payload.values.website` shape.\n *\n * `aria-hidden=\"true\"` + `tabIndex={-1}` keep the input out of the AT\n * tree and the keyboard tab order.\n */\nexport function HoneypotField({ labels }: HoneypotFieldProps) {\n  const { register } = useFormContext<SignupStep2Values>();\n  return (\n    <input\n      type=\"text\"\n      autoComplete=\"off\"\n      tabIndex={-1}\n      aria-hidden=\"true\"\n      aria-label={labels.honeypotLabel}\n      style={HONEYPOT_STYLE}\n      {...register(HONEYPOT_RHF_KEY as never)}\n      name={HONEYPOT_HTML_NAME}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/signup-form/parts/honeypot-field.tsx"
    },
    {
      "path": "src/registry/components/forms/signup-form/parts/optional-fields-fieldset.tsx",
      "content": "\"use client\";\n\nimport { useId } from \"react\";\nimport { useFormContext } from \"react-hook-form\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport type {\n  OptionalFieldConfig,\n  OptionalFieldName,\n  SignupLabels,\n  SignupStep2Values,\n} from \"../types\";\n\nconst OPTIONAL_FIELDS: ReadonlyArray<OptionalFieldName> = [\n  \"firstName\",\n  \"lastName\",\n  \"displayName\",\n  \"phone\",\n  \"company\",\n];\n\nconst AUTOCOMPLETE_MAP: Record<OptionalFieldName, string> = {\n  firstName: \"given-name\",\n  lastName: \"family-name\",\n  displayName: \"nickname\",\n  phone: \"tel\",\n  company: \"organization\",\n};\n\nconst INPUT_TYPE_MAP: Record<OptionalFieldName, string> = {\n  firstName: \"text\",\n  lastName: \"text\",\n  displayName: \"text\",\n  phone: \"tel\",\n  company: \"text\",\n};\n\nexport interface OptionalFieldsFieldsetProps {\n  fields: Partial<Record<OptionalFieldName, OptionalFieldConfig>>;\n  labels: Pick<\n    SignupLabels,\n    | \"firstNameLabel\"\n    | \"firstNamePlaceholder\"\n    | \"lastNameLabel\"\n    | \"lastNamePlaceholder\"\n    | \"displayNameLabel\"\n    | \"displayNamePlaceholder\"\n    | \"phoneLabel\"\n    | \"phonePlaceholder\"\n    | \"companyLabel\"\n    | \"companyPlaceholder\"\n  >;\n  /** Two-column grid at `sm:` breakpoint under `default`; single-column under `compact`. */\n  density: \"compact\" | \"default\";\n}\n\n/**\n * `<fieldset>` containing each opted-in optional field. Iterates a fixed\n * `OPTIONAL_FIELDS` list so the rendering order is deterministic.\n */\nexport function OptionalFieldsFieldset({\n  fields,\n  labels,\n  density,\n}: OptionalFieldsFieldsetProps) {\n  const visible = OPTIONAL_FIELDS.filter((name) => {\n    const config = fields[name];\n    return config !== undefined && config !== false;\n  });\n  if (visible.length === 0) return null;\n  return (\n    <fieldset\n      className={cn(\n        \"grid gap-3\",\n        density === \"compact\"\n          ? \"grid-cols-1\"\n          : \"grid-cols-1 sm:grid-cols-2\",\n      )}\n    >\n      {visible.map((name) => (\n        <OptionalField\n          key={name}\n          name={name}\n          required={\n            fields[name] === true ? false : (fields[name] as { required: boolean }).required\n          }\n          label={labels[`${name}Label` as keyof typeof labels]}\n          placeholder={labels[`${name}Placeholder` as keyof typeof labels]}\n        />\n      ))}\n    </fieldset>\n  );\n}\n\ninterface OptionalFieldProps {\n  name: OptionalFieldName;\n  required: boolean;\n  label: string;\n  placeholder: string;\n}\n\nfunction OptionalField({\n  name,\n  required,\n  label,\n  placeholder,\n}: OptionalFieldProps) {\n  const id = useId();\n  const errorId = `${id}-error`;\n  const {\n    register,\n    formState: { errors },\n  } = useFormContext<SignupStep2Values>();\n  const error = errors[name]?.message;\n  return (\n    <div className=\"flex flex-col gap-1.5\">\n      <Label htmlFor={id}>\n        {label}\n        {required ? <span aria-hidden=\"true\" className=\"ms-0.5 text-destructive\">*</span> : null}\n      </Label>\n      <Input\n        id={id}\n        type={INPUT_TYPE_MAP[name]}\n        autoComplete={AUTOCOMPLETE_MAP[name]}\n        placeholder={placeholder}\n        aria-required={required ? true : undefined}\n        aria-invalid={error ? true : undefined}\n        aria-describedby={error ? errorId : undefined}\n        {...register(name)}\n      />\n      <p\n        id={errorId}\n        role={error ? \"alert\" : undefined}\n        className={cn(\n          \"min-h-5 text-xs\",\n          error ? \"text-destructive\" : \"text-transparent\",\n        )}\n      >\n        {error ?? \"\"}\n      </p>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/signup-form/parts/optional-fields-fieldset.tsx"
    },
    {
      "path": "src/registry/components/forms/signup-form/parts/step-indicator.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport type { SignupLabels } from \"../types\";\n\nexport interface StepIndicatorProps {\n  current: 1 | 2;\n  total: number;\n  labels: Pick<SignupLabels, \"stepOf\">;\n}\n\n/**\n * `1 / 2` → `2 / 2` progress indicator for the two-step flow.\n * `role=\"status\" aria-live=\"polite\"` announces the step number on\n * transition. Hidden entirely under `flow === \"single-step\"` — root\n * component does not render this for single-step.\n */\nexport function StepIndicator({ current, total, labels }: StepIndicatorProps) {\n  const text = labels.stepOf\n    .replace(\"{current}\", String(current))\n    .replace(\"{total}\", String(total));\n  return (\n    <div\n      role=\"status\"\n      aria-live=\"polite\"\n      className=\"flex items-center gap-2 text-xs text-muted-foreground\"\n    >\n      <div className=\"flex gap-1\" aria-hidden=\"true\">\n        {Array.from({ length: total }).map((_, i) => (\n          <span\n            key={i}\n            className={cn(\n              \"h-1 w-6 rounded-full transition-colors\",\n              i + 1 <= current ? \"bg-primary\" : \"bg-border\",\n            )}\n          />\n        ))}\n      </div>\n      <span>{text}</span>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/signup-form/parts/step-indicator.tsx"
    },
    {
      "path": "src/registry/components/forms/signup-form/parts/step-shell.tsx",
      "content": "\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport type { FormStep } from \"../hooks/use-form-step\";\n\nexport interface StepShellProps {\n  step: FormStep;\n  children: ReactNode;\n}\n\n/**\n * Wrapper that fades content in when the step changes. The `key`\n * collapses the children when the step transitions so the CSS\n * keyframe (`signup-form-step-fade-in`) replays from `opacity: 0`.\n *\n * The fade is purely cosmetic; `prefers-reduced-motion: reduce`\n * collapses it to a 0ms hard-swap via the CSS rule in\n * `signup-form.css`. Hard-swap = no animation, which is the\n * correct a11y behavior.\n */\nexport function StepShell({ step, children }: StepShellProps) {\n  return (\n    <div\n      key={step}\n      data-signup-form-step={step}\n      className=\"flex flex-col gap-3\"\n    >\n      {children}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/signup-form/parts/step-shell.tsx"
    },
    {
      "path": "src/registry/components/forms/signup-form/parts/server-error.tsx",
      "content": "\"use client\";\n\nimport { AlertCircle } from \"lucide-react\";\n\nexport interface ServerErrorProps {\n  message: string;\n}\n\n/**\n * `role=\"alert\"` banner above the form for server-side errors (controlled\n * `errorMessage` prop). Returns null when no message — the caller checks\n * before mounting.\n */\nexport function ServerError({ message }: ServerErrorProps) {\n  return (\n    <div\n      role=\"alert\"\n      className=\"flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/5 p-3 text-sm text-destructive\"\n    >\n      <AlertCircle className=\"mt-0.5 size-4 shrink-0\" aria-hidden=\"true\" />\n      <span>{message}</span>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/signup-form/parts/server-error.tsx"
    },
    {
      "path": "src/registry/components/forms/signup-form/parts/success-screen.tsx",
      "content": "\"use client\";\n\nimport { Check } from \"lucide-react\";\nimport type { ReactNode } from \"react\";\n\nexport interface SuccessScreenProps {\n  message: ReactNode | string;\n}\n\n/**\n * `role=\"status\" aria-live=\"polite\"` region that replaces the form\n * after `onSubmit` resolves successfully. The caller swaps this in\n * place of the `<form>`.\n *\n * No \"Back to sign-up\" link by default — consumers either re-render\n * parent with a fresh `status: \"idle\"` or pop the user to a different\n * route (e.g., `/sign-in` after email verification).\n */\nexport function SuccessScreen({ message }: SuccessScreenProps) {\n  return (\n    <div\n      role=\"status\"\n      aria-live=\"polite\"\n      className=\"flex flex-col items-center gap-3 rounded-md border border-border bg-card p-6 text-center text-sm text-foreground\"\n    >\n      <div className=\"grid size-10 place-items-center rounded-full bg-primary/10 text-primary\">\n        <Check className=\"size-5\" aria-hidden=\"true\" />\n      </div>\n      <div className=\"text-base font-medium text-foreground\">{message}</div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/signup-form/parts/success-screen.tsx"
    },
    {
      "path": "src/registry/components/forms/signup-form/parts/submit-row.tsx",
      "content": "\"use client\";\n\nimport { Loader2 } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport type {\n  ButtonVariant,\n  SignupFormStatus,\n  SignupLabels,\n} from \"../types\";\n\nexport interface SubmitRowProps {\n  status: SignupFormStatus;\n  flow: \"single-step\" | \"two-step\";\n  step: \"step1\" | \"step2\";\n  skippableStepTwo: boolean;\n  submitLabel: string;\n  submitVariant?: ButtonVariant;\n  labels: Pick<SignupLabels, \"continueLabel\" | \"backLabel\" | \"skipForNowLabel\">;\n  onBack: () => void;\n  onSkip: () => void;\n}\n\n/**\n * Submit / Back / Skip-for-now button row.\n *\n * - Submit: always rendered, `type=\"submit\"`, disabled during submitting.\n *   On step-1 of two-step flow, label is `continueLabel`; otherwise it's\n *   the consumer-provided `submitLabel` (or default \"Create account\").\n * - Back: only on step-2 of two-step flow. `type=\"button\"` so it doesn't\n *   trip the form's submit handler.\n * - Skip-for-now: only on step-2 of two-step flow when `skippableStepTwo`\n *   is true. `type=\"button\"`.\n */\nexport function SubmitRow({\n  status,\n  flow,\n  step,\n  skippableStepTwo,\n  submitLabel,\n  submitVariant,\n  labels,\n  onBack,\n  onSkip,\n}: SubmitRowProps) {\n  const isSubmitting = status === \"submitting\";\n  const isStep1OfTwoStep = flow === \"two-step\" && step === \"step1\";\n  const isStep2OfTwoStep = flow === \"two-step\" && step === \"step2\";\n  const effectiveLabel = isStep1OfTwoStep ? labels.continueLabel : submitLabel;\n  const hasSecondaryActions = isStep2OfTwoStep;\n\n  const submitButton = (\n    <Button\n      type=\"submit\"\n      variant={submitVariant ?? \"default\"}\n      aria-busy={isSubmitting || undefined}\n      disabled={isSubmitting}\n      className={cn(!hasSecondaryActions && \"w-full\")}\n    >\n      <span className={cn(\"inline-flex items-center gap-2\")}>\n        {isSubmitting ? (\n          <Loader2 className=\"size-4 animate-spin\" aria-hidden=\"true\" />\n        ) : null}\n        {effectiveLabel}\n      </span>\n    </Button>\n  );\n\n  if (!hasSecondaryActions) {\n    return submitButton;\n  }\n\n  return (\n    <div className=\"flex flex-col-reverse gap-2 sm:flex-row sm:justify-between\">\n      <div className=\"flex gap-2\">\n        <Button\n          type=\"button\"\n          variant=\"ghost\"\n          onClick={onBack}\n          disabled={isSubmitting}\n        >\n          {labels.backLabel}\n        </Button>\n        {skippableStepTwo ? (\n          <Button\n            type=\"button\"\n            variant=\"outline\"\n            onClick={onSkip}\n            disabled={isSubmitting}\n          >\n            {labels.skipForNowLabel}\n          </Button>\n        ) : null}\n      </div>\n      {submitButton}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/signup-form/parts/submit-row.tsx"
    },
    {
      "path": "src/registry/components/forms/signup-form/parts/sign-in-link.tsx",
      "content": "\"use client\";\n\nimport type { SignupLabels } from \"../types\";\n\nexport interface SignInLinkProps {\n  href: string;\n  labels: Pick<SignupLabels, \"signInLabel\">;\n}\n\n/**\n * \"Already have an account? Sign in\" link below the form. Plain `<a>`\n * (registry portability lock — can't import `next/link`). Caller checks\n * for `signInHref` and skips mounting if undefined.\n */\nexport function SignInLink({ href, labels }: SignInLinkProps) {\n  return (\n    <p className=\"text-center text-sm text-muted-foreground\">\n      <a\n        href={href}\n        className=\"underline underline-offset-2 hover:text-foreground\"\n      >\n        {labels.signInLabel}\n      </a>\n    </p>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/signup-form/parts/sign-in-link.tsx"
    },
    {
      "path": "src/registry/components/forms/signup-form/parts/oauth-row.tsx",
      "content": "\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { getOAuthProviderLabel } from \"../lib/oauth-providers\";\nimport type { OAuthProvider, SignupLabels } from \"../types\";\n\nexport interface OauthRowProps {\n  providers: ReadonlyArray<OAuthProvider>;\n  icons?: Partial<Record<OAuthProvider, ReactNode>>;\n  labels: Pick<SignupLabels, \"orContinueWith\" | \"oauthLabelTemplate\">;\n  disabled?: boolean;\n  onClick: (provider: OAuthProvider) => void;\n}\n\n/**\n * OAuth button row + \"or\" divider. Stacks vertically on mobile,\n * flex-row on `sm:` breakpoint and up. Each button renders an optional\n * consumer-supplied icon (`oauthIcons` slot) followed by the\n * `{provider}`-interpolated template label.\n *\n * Returns `null` when `providers` is empty so the caller doesn't need\n * to guard the mount.\n */\nexport function OauthRow({\n  providers,\n  icons,\n  labels,\n  disabled,\n  onClick,\n}: OauthRowProps) {\n  if (providers.length === 0) return null;\n  return (\n    <div className=\"flex flex-col gap-3\">\n      <div className=\"flex flex-col gap-2 sm:flex-row sm:gap-2\">\n        {providers.map((provider) => (\n          <Button\n            key={provider}\n            type=\"button\"\n            variant=\"outline\"\n            className=\"w-full sm:flex-1\"\n            disabled={disabled}\n            onClick={() => onClick(provider)}\n          >\n            <span className=\"inline-flex items-center gap-2\">\n              {icons?.[provider] ?? null}\n              {getOAuthProviderLabel(provider, labels.oauthLabelTemplate)}\n            </span>\n          </Button>\n        ))}\n      </div>\n      <div\n        role=\"separator\"\n        aria-orientation=\"horizontal\"\n        aria-label={labels.orContinueWith}\n        className=\"flex items-center gap-3 text-xs uppercase tracking-wide text-muted-foreground\"\n      >\n        <span aria-hidden=\"true\" className=\"h-px flex-1 bg-border\" />\n        <span aria-hidden=\"true\">{labels.orContinueWith}</span>\n        <span aria-hidden=\"true\" className=\"h-px flex-1 bg-border\" />\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/signup-form/parts/oauth-row.tsx"
    },
    {
      "path": "src/registry/components/forms/signup-form/parts/strength-meter.tsx",
      "content": "\"use client\";\n\nimport { useStrengthMeter, type StrengthLabels } from \"../hooks/use-strength-meter\";\nimport { cn } from \"@/lib/utils\";\nimport type { StrengthCalculator } from \"../types\";\n\nexport interface StrengthMeterProps {\n  calculator?: StrengthCalculator;\n  labels: StrengthLabels;\n}\n\n/**\n * 4-segment password strength bar + descriptive text. The bar is\n * `role=\"presentation\"` (the textual companion is the AT-readable\n * surface; the bar is purely visual). The companion has\n * `aria-live=\"polite\"` so SR users hear \"Strength: strong\" on update.\n *\n * Segments highlight from left to right based on the score (0–4).\n * Color gradient mirrors json-form's strength conventions: destructive\n * → amber → primary (signal-lime) for \"fair / strong / excellent\".\n */\nexport function StrengthMeter({ calculator, labels }: StrengthMeterProps) {\n  const { score, label } = useStrengthMeter({ calculator, labels });\n  return (\n    <div className=\"flex flex-col gap-1\">\n      <div\n        role=\"presentation\"\n        aria-hidden=\"true\"\n        className=\"flex gap-1\"\n      >\n        {[1, 2, 3, 4].map((segment) => (\n          <span\n            key={segment}\n            className={cn(\n              \"h-1 flex-1 rounded-full transition-colors\",\n              segment > score\n                ? \"bg-border\"\n                : score === 1\n                  ? \"bg-destructive\"\n                  : score === 2\n                    ? \"bg-amber-500\"\n                    : \"bg-primary\",\n            )}\n          />\n        ))}\n      </div>\n      <span\n        aria-live=\"polite\"\n        className=\"min-h-4 text-xs text-muted-foreground\"\n      >\n        {label}\n      </span>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/signup-form/parts/strength-meter.tsx"
    },
    {
      "path": "src/registry/components/forms/signup-form/parts/labels.ts",
      "content": "import type { SignupLabels } from \"../types\";\n\n/**\n * Default English labels for `<SignupForm>`. Consumers override\n * any subset via the `labels?: Partial<SignupLabels>` prop;\n * `mergeSignupLabels` shallow-merges the override over these\n * defaults at the top of the component.\n *\n * The shape mirrors json-form's `defaultJsonFormStrings` + `mergeJsonFormStrings`\n * pattern.\n */\nexport const defaultSignupLabels: SignupLabels = {\n  // Field labels\n  emailLabel: \"Email\",\n  emailPlaceholder: \"you@example.com\",\n  passwordLabel: \"Password\",\n  passwordPlaceholder: \"At least 8 characters\",\n  firstNameLabel: \"First name\",\n  firstNamePlaceholder: \"\",\n  lastNameLabel: \"Last name\",\n  lastNamePlaceholder: \"\",\n  displayNameLabel: \"Display name\",\n  displayNamePlaceholder: \"\",\n  phoneLabel: \"Phone\",\n  phonePlaceholder: \"\",\n  companyLabel: \"Company\",\n  companyPlaceholder: \"\",\n\n  // Buttons\n  createAccountLabel: \"Create account\",\n  continueLabel: \"Continue\",\n  backLabel: \"Back\",\n  skipForNowLabel: \"Skip for now\",\n  signInLabel: \"Already have an account? Sign in\",\n\n  // OAuth\n  orContinueWith: \"or\",\n  oauthLabelTemplate: \"Continue with {provider}\",\n\n  // Step indicator\n  stepOf: \"Step {current} of {total}\",\n\n  // Error messages\n  emailRequired: \"Email is required\",\n  emailInvalid: \"Enter a valid email\",\n  passwordRequired: \"Password is required\",\n  passwordTooShort: \"Password must be at least {min} characters\",\n  passwordMissingUppercase: \"Password must contain an uppercase letter\",\n  passwordMissingNumber: \"Password must contain a number\",\n  passwordMissingSymbol: \"Password must contain a symbol\",\n  consentRequired: \"You must accept the terms to continue\",\n\n  // Strength meter\n  strengthWeak: \"weak\",\n  strengthFair: \"fair\",\n  strengthStrong: \"strong\",\n  strengthExcellent: \"excellent\",\n  strengthLabel: \"Strength: {level}\",\n\n  // Server-error fallback\n  serverError: \"Something went wrong. Please try again.\",\n\n  // Success\n  successDefaultMessage:\n    \"You're in. Check your inbox to verify your email.\",\n\n  // sr-only accessibility hints\n  passwordToggleShow: \"Show password\",\n  passwordToggleHide: \"Hide password\",\n  honeypotLabel: \"Leave this field empty\",\n};\n\n/**\n * Shallow-merge consumer overrides over the English defaults. Returns\n * the full, populated `SignupLabels` object so downstream parts\n * can read by key without optional-chaining.\n */\nexport function mergeSignupLabels(\n  override?: Partial<SignupLabels>,\n): SignupLabels {\n  if (!override) return defaultSignupLabels;\n  return { ...defaultSignupLabels, ...override };\n}\n",
      "type": "registry:component",
      "target": "components/signup-form/parts/labels.ts"
    },
    {
      "path": "src/registry/components/forms/signup-form/hooks/use-signup-form.ts",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { useForm, type UseFormReturn } from \"react-hook-form\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport {\n  buildSchema,\n  type SchemaLabels,\n} from \"../lib/build-schema\";\nimport {\n  HONEYPOT_RHF_KEY,\n  isHoneypotTripped as computeHoneypotTripped,\n} from \"../lib/honeypot\";\nimport type {\n  OptionalFieldConfig,\n  OptionalFieldName,\n  SignupFormProps,\n  SignupFormStatus,\n  SignupLabels,\n  SignupStep1Values,\n  SignupStep2Values,\n  SignupSubmitPayload,\n} from \"../types\";\nimport { useFormStep, type FormStep } from \"./use-form-step\";\n\nconst DEFAULT_POLICY = {\n  minLength: 8,\n  requireUppercase: false,\n  requireNumber: false,\n  requireSymbol: false,\n};\n\nexport interface UseSignupFormArgs {\n  flow: \"single-step\" | \"two-step\";\n  passwordStrategy: \"password\" | \"magic-link\";\n  skippableStepTwo: boolean;\n  fields: Partial<Record<OptionalFieldName, OptionalFieldConfig>>;\n  passwordPolicy: SignupFormProps[\"passwordPolicy\"];\n  consent: SignupFormProps[\"consent\"];\n  labels: SignupLabels;\n  status?: SignupFormStatus;\n  onStatusChange?: (status: SignupFormStatus) => void;\n  onSubmit: (payload: SignupSubmitPayload) => void | Promise<void>;\n}\n\nexport interface UseSignupFormReturn {\n  form: UseFormReturn<SignupStep2Values>;\n  status: SignupFormStatus;\n  step: FormStep;\n  /** Step1 → step2 (two-step). Returns whether the transition succeeded. */\n  goNext: () => Promise<boolean>;\n  /** Step2 → step1 (two-step). */\n  goBack: () => void;\n  /** Submit the form. Validates first; transitions status; calls onSubmit with the envelope. */\n  submit: () => Promise<void>;\n  /** Skip-for-now: submit with step-1 values only (`stepCompleted: \"step1\"`). Two-step + skippable only. */\n  skip: () => Promise<void>;\n}\n\n/**\n * Substrate hook. Owns the RHF + Zod glue, the status state machine,\n * and the discriminated submit envelope assembly.\n *\n * **Mutual-exclusion contract (Q5 lock):** if `props.status` is provided,\n * the internal `setStatus` is a no-op for the internal state — the\n * component reads `props.status` as source of truth. `onStatusChange`\n * fires on every computed transition regardless (so observers can react\n * to \"what the status WOULD be\" in controlled mode).\n */\nexport function useSignupForm(\n  args: UseSignupFormArgs,\n): UseSignupFormReturn {\n  const {\n    flow,\n    passwordStrategy,\n    skippableStepTwo,\n    fields,\n    passwordPolicy: rawPolicy,\n    consent,\n    labels,\n    status: controlledStatus,\n    onStatusChange,\n    onSubmit,\n  } = args;\n\n  const isControlled = controlledStatus !== undefined;\n  const [internalStatus, setInternalStatus] =\n    useState<SignupFormStatus>(\"idle\");\n\n  // The onStatusChange callback can change identity every render if the\n  // consumer passes an inline arrow function. Stable ref keeps our\n  // setStatus identity steady so memoized actions don't re-bind every\n  // render.\n  const onStatusChangeRef = useRef(onStatusChange);\n  useEffect(() => {\n    onStatusChangeRef.current = onStatusChange;\n  }, [onStatusChange]);\n\n  const setStatus = useCallback(\n    (next: SignupFormStatus) => {\n      if (!isControlled) setInternalStatus(next);\n      onStatusChangeRef.current?.(next);\n    },\n    [isControlled],\n  );\n\n  const status: SignupFormStatus = isControlled\n    ? controlledStatus!\n    : internalStatus;\n\n  // Normalize the password policy: shipped + un-shipped flags both\n  // collapse to the same shape used by build-schema.\n  const policy = useMemo(\n    () => ({\n      minLength: rawPolicy?.minLength ?? DEFAULT_POLICY.minLength,\n      requireUppercase:\n        rawPolicy?.requireUppercase ?? DEFAULT_POLICY.requireUppercase,\n      requireNumber: rawPolicy?.requireNumber ?? DEFAULT_POLICY.requireNumber,\n      requireSymbol: rawPolicy?.requireSymbol ?? DEFAULT_POLICY.requireSymbol,\n    }),\n    [\n      rawPolicy?.minLength,\n      rawPolicy?.requireUppercase,\n      rawPolicy?.requireNumber,\n      rawPolicy?.requireSymbol,\n    ],\n  );\n\n  const schemaLabels = useMemo<SchemaLabels>(\n    () => ({\n      emailRequired: labels.emailRequired,\n      emailInvalid: labels.emailInvalid,\n      passwordRequired: labels.passwordRequired,\n      passwordTooShort: labels.passwordTooShort,\n      passwordMissingUppercase: labels.passwordMissingUppercase,\n      passwordMissingNumber: labels.passwordMissingNumber,\n      passwordMissingSymbol: labels.passwordMissingSymbol,\n      consentRequired: labels.consentRequired,\n    }),\n    [\n      labels.emailRequired,\n      labels.emailInvalid,\n      labels.passwordRequired,\n      labels.passwordTooShort,\n      labels.passwordMissingUppercase,\n      labels.passwordMissingNumber,\n      labels.passwordMissingSymbol,\n      labels.consentRequired,\n    ],\n  );\n\n  const schema = useMemo(\n    () =>\n      buildSchema({\n        passwordStrategy,\n        passwordPolicy: policy,\n        fields,\n        consent: { required: consent.required },\n        labels: schemaLabels,\n      }),\n    [passwordStrategy, policy, fields, consent.required, schemaLabels],\n  );\n\n  const form = useForm<SignupStep2Values>({\n    // The resolver-cast pattern matches json-form's\n    // `use-json-form.ts` precedent (RHF / Zod overload disambiguation).\n    resolver: zodResolver(schema as never) as never,\n    mode: \"onSubmit\",\n    defaultValues: {\n      email: \"\",\n      password: passwordStrategy === \"password\" ? \"\" : undefined,\n      consentAccepted: false,\n      [HONEYPOT_RHF_KEY]: \"\",\n    } as SignupStep2Values,\n  });\n\n  const stepHook = useFormStep({\n    enabled: flow === \"two-step\",\n    form,\n  });\n\n  const submitInternal = useCallback(\n    async (stepCompleted: \"single\" | \"step1\" | \"step2\") => {\n      setStatus(\"submitting\");\n      try {\n        // form.handleSubmit returns a function that runs validation +\n        // either the valid handler or the invalid handler. We invoke it\n        // inline so the `await onSubmit(...)` happens within the same\n        // try/catch.\n        let didFire = false;\n        await new Promise<void>((resolve, reject) => {\n          void form.handleSubmit(\n            async (values) => {\n              didFire = true;\n              const honeypotValue =\n                (values as unknown as Record<string, unknown>)[HONEYPOT_RHF_KEY] ?? \"\";\n              const isHoneypotTripped = computeHoneypotTripped(\n                String(honeypotValue),\n              );\n              const payload = buildPayload(\n                stepCompleted,\n                values,\n                isHoneypotTripped,\n              );\n              try {\n                await onSubmit(payload);\n                resolve();\n              } catch (err) {\n                reject(err);\n              }\n            },\n            () => {\n              // Invalid handler — validation failed. We don't transition\n              // to \"error\" here (that's reserved for onSubmit-throws);\n              // RHF surfaces the per-field errors via formState.\n              resolve();\n            },\n          )();\n          if (!didFire) {\n            // Edge: handleSubmit may resolve synchronously when there's\n            // no async work; the resolve() above covers that.\n          }\n        });\n        if (didFire) setStatus(\"success\");\n        else setStatus(\"idle\"); // validation failed; back to idle\n      } catch {\n        setStatus(\"error\");\n      }\n    },\n    [form, onSubmit, setStatus],\n  );\n\n  const submit = useCallback(async () => {\n    const stepCompleted: \"single\" | \"step2\" =\n      flow === \"single-step\" ? \"single\" : \"step2\";\n    await submitInternal(stepCompleted);\n  }, [flow, submitInternal]);\n\n  const skip = useCallback(async () => {\n    if (flow !== \"two-step\" || !skippableStepTwo) return;\n    // Validate step-1 fields only.\n    const ok = await form.trigger([\n      \"email\",\n      \"password\",\n      \"consentAccepted\",\n    ] as const);\n    if (!ok) return;\n    setStatus(\"submitting\");\n    try {\n      const values = form.getValues();\n      const honeypotValue =\n        (values as unknown as Record<string, unknown>)[HONEYPOT_RHF_KEY] ?? \"\";\n      const isHoneypotTripped = computeHoneypotTripped(String(honeypotValue));\n      const step1Values: SignupStep1Values = {\n        email: values.email,\n        password: values.password,\n        consentAccepted: values.consentAccepted,\n        _honeypot: String(honeypotValue),\n      };\n      await onSubmit({\n        stepCompleted: \"step1\",\n        values: step1Values,\n        isHoneypotTripped,\n      });\n      setStatus(\"success\");\n    } catch {\n      setStatus(\"error\");\n    }\n  }, [flow, skippableStepTwo, form, onSubmit, setStatus]);\n\n  return {\n    form,\n    status,\n    step: stepHook.step,\n    goNext: stepHook.goNext,\n    goBack: stepHook.goBack,\n    submit,\n    skip,\n  };\n}\n\nfunction buildPayload(\n  stepCompleted: \"single\" | \"step1\" | \"step2\",\n  values: SignupStep2Values,\n  isHoneypotTripped: boolean,\n): SignupSubmitPayload {\n  if (stepCompleted === \"step1\") {\n    return {\n      stepCompleted: \"step1\",\n      values: {\n        email: values.email,\n        password: values.password,\n        consentAccepted: values.consentAccepted,\n        _honeypot: values._honeypot,\n      },\n      isHoneypotTripped,\n    };\n  }\n  return {\n    stepCompleted,\n    values,\n    isHoneypotTripped,\n  };\n}\n",
      "type": "registry:component",
      "target": "components/signup-form/hooks/use-signup-form.ts"
    },
    {
      "path": "src/registry/components/forms/signup-form/hooks/use-strength-meter.ts",
      "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport { useWatch } from \"react-hook-form\";\nimport { defaultStrengthCalculator } from \"../lib/strength-calculator\";\nimport type { SignupLabels, StrengthCalculator } from \"../types\";\n\nexport type StrengthLabels = Pick<\n  SignupLabels,\n  | \"strengthWeak\"\n  | \"strengthFair\"\n  | \"strengthStrong\"\n  | \"strengthExcellent\"\n  | \"strengthLabel\"\n>;\n\nexport interface UseStrengthMeterArgs {\n  calculator?: StrengthCalculator;\n  labels: StrengthLabels;\n}\n\nexport interface UseStrengthMeterReturn {\n  score: 0 | 1 | 2 | 3 | 4;\n  label: string;\n}\n\n/**\n * Narrow-deps password-strength hook. Subscribes to the `password` field\n * via RHF's `useWatch({ name })` against the active FormProvider context;\n * re-computes when the password changes. The consumer-supplied\n * `strengthCalculator` is clamped defensively so a buggy BYO heuristic\n * returning out-of-range values can't crash the meter.\n *\n * `label` interpolates `labels.strengthLabel` (`\"Strength: {level}\"`)\n * with the bucketed `strengthWeak`/`Fair`/`Strong`/`Excellent` text. For\n * score 0 (empty), `label` is the empty string — the meter is rendered\n * but the descriptive text disappears.\n */\nexport function useStrengthMeter(\n  args: UseStrengthMeterArgs,\n): UseStrengthMeterReturn {\n  const { calculator, labels } = args;\n  const password = useWatch({ name: \"password\" }) as string | undefined;\n\n  return useMemo<UseStrengthMeterReturn>(() => {\n    const calc = calculator ?? defaultStrengthCalculator;\n    const raw = calc(password ?? \"\");\n    const score = clampScore(raw);\n    if (score === 0) return { score, label: \"\" };\n    const levelText =\n      score === 1\n        ? labels.strengthWeak\n        : score === 2\n          ? labels.strengthFair\n          : score === 3\n            ? labels.strengthStrong\n            : labels.strengthExcellent;\n    return { score, label: labels.strengthLabel.replace(\"{level}\", levelText) };\n  }, [calculator, password, labels]);\n}\n\nfunction clampScore(raw: number): 0 | 1 | 2 | 3 | 4 {\n  if (raw <= 0) return 0;\n  if (raw === 1) return 1;\n  if (raw === 2) return 2;\n  if (raw === 3) return 3;\n  return 4;\n}\n",
      "type": "registry:component",
      "target": "components/signup-form/hooks/use-strength-meter.ts"
    },
    {
      "path": "src/registry/components/forms/signup-form/hooks/use-form-step.ts",
      "content": "\"use client\";\n\nimport { useCallback, useMemo, useState } from \"react\";\nimport type { UseFormReturn } from \"react-hook-form\";\nimport type {\n  SignupStep1Values,\n  SignupStep2Values,\n} from \"../types\";\n\nexport type FormStep = \"step1\" | \"step2\";\n\nexport interface UseFormStepArgs {\n  /** When `false`, the hook locks step at `\"step1\"` and goNext/goBack are no-ops. */\n  enabled: boolean;\n  form: UseFormReturn<SignupStep2Values>;\n}\n\nexport interface UseFormStepReturn {\n  step: FormStep;\n  /** Validate step-1 fields; advance to step-2 only if valid. Returns whether the transition succeeded. */\n  goNext: () => Promise<boolean>;\n  goBack: () => void;\n}\n\n/**\n * Step-state hook for the two-step flow. Encapsulates:\n *  - `useState<FormStep>(\"step1\")`\n *  - Partial validation of step-1 fields via `form.trigger([...])`\n *  - No-op behavior under `flow === \"single-step\"` (enabled=false) so the\n *    hook is always called for hook-order stability without affecting\n *    behavior.\n *\n * Step-1 fields = `email`, `password` (when present), `consentAccepted`.\n * Honeypot is NOT in the step-1 trigger list — it's intentionally\n * \"validated\" at submit time (where we read it for the trip flag) and\n * has no Zod check.\n */\nexport function useFormStep(args: UseFormStepArgs): UseFormStepReturn {\n  const { enabled, form } = args;\n  const [step, setStep] = useState<FormStep>(\"step1\");\n\n  const step1Fields = useMemo<\n    Array<keyof SignupStep1Values>\n  >(() => [\"email\", \"password\", \"consentAccepted\"], []);\n\n  const goNext = useCallback(async () => {\n    if (!enabled) return false;\n    if (step === \"step2\") return true;\n    const ok = await form.trigger(step1Fields);\n    if (ok) setStep(\"step2\");\n    return ok;\n  }, [enabled, step, form, step1Fields]);\n\n  const goBack = useCallback(() => {\n    if (!enabled) return;\n    setStep(\"step1\");\n  }, [enabled]);\n\n  return { step, goNext, goBack };\n}\n",
      "type": "registry:component",
      "target": "components/signup-form/hooks/use-form-step.ts"
    },
    {
      "path": "src/registry/components/forms/signup-form/lib/build-schema.ts",
      "content": "import { z, type ZodTypeAny } from \"zod\";\nimport type {\n  OptionalFieldConfig,\n  OptionalFieldName,\n  SignupLabels,\n} from \"../types\";\n\nconst OPTIONAL_FIELDS: ReadonlyArray<OptionalFieldName> = [\n  \"firstName\",\n  \"lastName\",\n  \"displayName\",\n  \"phone\",\n  \"company\",\n];\n\n/**\n * Subset of `SignupLabels` carrying the error-message templates the\n * Zod schema bakes in. The hook passes the full merged labels bag; we\n * narrow here to make the function pure-ish.\n */\nexport type SchemaLabels = Pick<\n  SignupLabels,\n  | \"emailRequired\"\n  | \"emailInvalid\"\n  | \"passwordRequired\"\n  | \"passwordTooShort\"\n  | \"passwordMissingUppercase\"\n  | \"passwordMissingNumber\"\n  | \"passwordMissingSymbol\"\n  | \"consentRequired\"\n>;\n\nexport interface BuildSchemaArgs {\n  passwordStrategy: \"password\" | \"magic-link\";\n  passwordPolicy: {\n    minLength: number;\n    requireUppercase: boolean;\n    requireNumber: boolean;\n    requireSymbol: boolean;\n  };\n  fields: Partial<Record<OptionalFieldName, OptionalFieldConfig>>;\n  consent: { required: boolean };\n  labels: SchemaLabels;\n}\n\n/**\n * Build the Zod schema for the whole form (step 1 + step 2 fields).\n *\n * The schema is **single** — step-1 vs step-2 partial validation is\n * driven by `form.trigger(name[])` at the call site in the hook (NOT by\n * swapping schemas). Step-2-only fields are always optional at the type\n * level so the \"Skip for now\" path can submit with step-1 values alone\n * without tripping a \"required\" error on the skipped step-2 fields.\n *\n * Field-by-field rules:\n * - `email`: required, valid email format (Zod's `email()` check).\n * - `password`: optional under `magic-link`; under `password`, required\n *   + `minLength` + each policy class check.\n * - `consentAccepted`: required `true` when `consent.required`; otherwise\n *   just `boolean` (no validation).\n * - `_honeypot`: passthrough string. **Never validated** — the honeypot\n *   \"trip\" semantics live in `isHoneypotTripped()` at the submit handler,\n *   not in the schema. Validating it here would block submission on a\n *   bot fill, which is the WRONG behavior (we want the bot to think it\n *   succeeded).\n * - Optional fields (`firstName` etc.): present in the schema with\n *   `.optional()` if the consumer opted them in via `fields`. If\n *   `{ required: true }` on the optional-field config AND the user\n *   completes step 2, the field is required. The \"Skip for now\" path\n *   bypasses step-2 validation entirely (see hook).\n */\nexport function buildSchema(args: BuildSchemaArgs) {\n  const { passwordStrategy, passwordPolicy, fields, consent, labels } = args;\n\n  const emailSchema = z\n    .string({ message: labels.emailRequired })\n    .min(1, labels.emailRequired)\n    .email(labels.emailInvalid);\n\n  let passwordSchema: ZodTypeAny;\n  if (passwordStrategy === \"magic-link\") {\n    // Field absent at runtime under magic-link. We still register a\n    // schema slot for shape stability (`.optional()`) so partial values\n    // shapes line up between the two strategies.\n    passwordSchema = z.string().optional();\n  } else {\n    let chain = z\n      .string({ message: labels.passwordRequired })\n      .min(\n        passwordPolicy.minLength,\n        labels.passwordTooShort.replace(\n          \"{min}\",\n          String(passwordPolicy.minLength),\n        ),\n      );\n    if (passwordPolicy.requireUppercase) {\n      chain = chain.refine((s) => /[A-Z]/.test(s), {\n        message: labels.passwordMissingUppercase,\n      });\n    }\n    if (passwordPolicy.requireNumber) {\n      chain = chain.refine((s) => /[0-9]/.test(s), {\n        message: labels.passwordMissingNumber,\n      });\n    }\n    if (passwordPolicy.requireSymbol) {\n      chain = chain.refine((s) => /[^A-Za-z0-9]/.test(s), {\n        message: labels.passwordMissingSymbol,\n      });\n    }\n    passwordSchema = chain;\n  }\n\n  const consentSchema = consent.required\n    ? z.literal(true, {\n        message: labels.consentRequired,\n      })\n    : z.boolean();\n\n  const shape: Record<string, ZodTypeAny> = {\n    email: emailSchema,\n    password: passwordSchema,\n    consentAccepted: consentSchema,\n    _honeypot: z.string(),\n  };\n\n  for (const name of OPTIONAL_FIELDS) {\n    const config = fields[name];\n    if (config === undefined || config === false) continue;\n    const required = config === true ? false : config.required;\n    shape[name] = required ? z.string().min(1) : z.string().optional();\n  }\n\n  return z.object(shape);\n}\n",
      "type": "registry:component",
      "target": "components/signup-form/lib/build-schema.ts"
    },
    {
      "path": "src/registry/components/forms/signup-form/lib/honeypot.ts",
      "content": "import type { CSSProperties } from \"react\";\n\n/**\n * The rendered HTML `name` attribute on the honeypot input. Tuned to the\n * spam-bait names most form-fill bots target (`url`, `website`,\n * `homepage`); `website` has the broadest hit-rate. **This is NOT the\n * RHF register key** — see `HONEYPOT_RHF_KEY` below.\n */\nexport const HONEYPOT_HTML_NAME = \"website\";\n\n/**\n * The RHF register key + values-bag key. The `_` prefix keeps it out of\n * the natural reading order of `payload.values` (consumers see\n * `payload.values._honeypot` and immediately read it as a meta field\n * rather than mistaking it for a real `website` field).\n */\nexport const HONEYPOT_RHF_KEY = \"_honeypot\";\n\n/**\n * Off-screen rendering style. **Critical: `display: none` is\n * bot-detectable — bots skip non-visible fields.** The off-screen\n * absolute-position pattern keeps the field rendered (and fillable by\n * a naïve form-fill bot) while invisible to real users.\n *\n * The 1×1 pixel sizing + `overflow: hidden` prevents any focus-ring\n * leakage if the field somehow receives focus. `tabIndex={-1}` (set on\n * the input element, not here) keeps it off keyboard tab order;\n * `aria-hidden=\"true\"` keeps it off the AT tree.\n */\nexport const HONEYPOT_STYLE: CSSProperties = {\n  position: \"absolute\",\n  left: \"-9999px\",\n  top: \"auto\",\n  width: \"1px\",\n  height: \"1px\",\n  overflow: \"hidden\",\n};\n\n/**\n * A real user cannot tab into the honeypot field (tabIndex={-1}) and\n * cannot see it (off-screen), so any non-empty value means a spam bot\n * filled it. The submit handler surfaces this on the payload via\n * `isHoneypotTripped: boolean`.\n */\nexport function isHoneypotTripped(value: string): boolean {\n  return value.length > 0;\n}\n",
      "type": "registry:component",
      "target": "components/signup-form/lib/honeypot.ts"
    },
    {
      "path": "src/registry/components/forms/signup-form/lib/oauth-providers.ts",
      "content": "import type { OAuthProvider } from \"../types\";\n\n/**\n * Built-in OAuth provider list — the runtime equivalent of the\n * `OAuthProvider` type literal union. Used to validate / iterate\n * consumer-provided `oauthProviders` arrays.\n */\nexport const OAUTH_PROVIDERS: ReadonlyArray<OAuthProvider> = [\n  \"google\",\n  \"github\",\n  \"apple\",\n  \"facebook\",\n  \"microsoft\",\n  \"x\",\n];\n\n/**\n * Per-provider display name. Used by `getOAuthProviderLabel` to\n * interpolate into `labels.oauthLabelTemplate`. Apple's brand guidelines\n * require \"Sign in with Apple\" specifically — but this component is\n * neutral on the verb (`Continue with` / `Sign up with`), so we surface\n * the capitalized provider name and let `oauthLabelTemplate` carry the\n * verb.\n */\nconst PROVIDER_DISPLAY_NAMES: Record<OAuthProvider, string> = {\n  google: \"Google\",\n  github: \"GitHub\",\n  apple: \"Apple\",\n  facebook: \"Facebook\",\n  microsoft: \"Microsoft\",\n  x: \"X\",\n};\n\n/**\n * Interpolate the provider name into the consumer-overridable template\n * (`labels.oauthLabelTemplate`). Default template is\n * `\"Continue with {provider}\"` — yields `\"Continue with Google\"`.\n */\nexport function getOAuthProviderLabel(\n  provider: OAuthProvider,\n  template: string,\n): string {\n  return template.replace(\"{provider}\", PROVIDER_DISPLAY_NAMES[provider]);\n}\n",
      "type": "registry:component",
      "target": "components/signup-form/lib/oauth-providers.ts"
    },
    {
      "path": "src/registry/components/forms/signup-form/lib/strength-calculator.ts",
      "content": "import type { StrengthCalculator } from \"../types\";\n\n/**\n * Built-in password-strength heuristic. Returns 0 for an empty string,\n * 1–4 for non-empty inputs based on length + character-class diversity.\n *\n * Algorithm:\n * - length 0 → 0 (empty / unrated)\n * - score = floor(length / 4) + characterClassCount − 1, clamped to [1, 4]\n *   - length 4 contributes +1; length 12 contributes +3\n *   - character classes = { has-lowercase, has-uppercase, has-digit,\n *     has-symbol } — each adds +1\n *   - then subtract 1 so a single-class short password lands at \"weak\"\n *\n * This is intentionally not a zxcvbn-style dictionary check — that would\n * pull a ~400KB peer dep for marginal value at v0.1. Consumers with\n * corporate password policies (or who want zxcvbn) plug a different\n * calculator via the `strengthCalculator` prop on `<SignupForm>`.\n */\nexport const defaultStrengthCalculator: StrengthCalculator = (password) => {\n  if (!password) return 0;\n\n  let classes = 0;\n  if (/[a-z]/.test(password)) classes++;\n  if (/[A-Z]/.test(password)) classes++;\n  if (/[0-9]/.test(password)) classes++;\n  if (/[^A-Za-z0-9]/.test(password)) classes++;\n\n  const lengthScore = Math.floor(password.length / 4);\n  const raw = lengthScore + classes - 1;\n\n  if (raw <= 1) return 1;\n  if (raw === 2) return 2;\n  if (raw === 3) return 3;\n  return 4;\n};\n",
      "type": "registry:component",
      "target": "components/signup-form/lib/strength-calculator.ts"
    }
  ],
  "categories": [
    "forms"
  ],
  "type": "registry:block"
}