Skip to content
  • Introduction
    nativecn is shadcn for Expo apps: Components you copy into your app and own, built so AI agents can build and verify whole mobile apps.
  • Installation
    Create a new Expo app with nativecn, or add nativecn to an existing one.
  • components.json
    The project file that records your Preset, Structure, aliases and agents.
  • CLI
    Every nativecn-cli command and flag: init, create, add, agents and mcp.
  • Changelog
    What changed in each nativecn release.
  • Theming
    Tokens, Colour Roles, Scale, Scheme, createStyles and fonts.
  • Presets & Styles
    Style, Base Colour, Accent Colour, Radius and fonts: chosen once, baked into your code.
  • Folder structure
    Flat and feature Structures, Destinations and the placement rule.
  • AI agents
    Rules, Skills, the nativecn MCP server, the Plugin and the Visual QA Loop.
  • Alert
    Alert, AlertTitle and AlertDescription: an inline message with Variants (default, destructive, success, warning, info), each with a default icon.
  • Avatar
    Round user image (expo-image) with fallback initials, in sizes sm, md and lg.
  • Badge
    Small status label with Variants default, secondary, outline, destructive, success and warning.
  • Button
    Button with Variants (primary, secondary, outline, ghost, destructive, link), Sizes, an icon slot, loading and status; icon-only requires aria-label.
  • Card
    Card surface with CardHeader, CardTitle, CardDescription, CardContent and CardFooter; onPress makes the whole card pressable.
  • Checkbox
    Checkbox with an optional label, controlled or uncontrolled, an indeterminate (mixed) state and a haptic tick.
  • Chip
    Chip (plain, toggle or removable, with an optional icon) and ChipGroup (single or multiple choice on the SelectionGroup Primitive), with a haptic tick.
  • Container
    Wraps a Screen: safe-area edges, scrolling, keyboard avoidance, Token padding and a max content width.
  • Drawer
    Drawer panel content for Expo Router's drawer (expo-router/drawer): DrawerContent, DrawerHeader, DrawerSection, DrawerItem (label, icon, href, badge; active from the current route) and DrawerFooter.
  • Empty State
    What a Screen or list shows when there is nothing to show yet: an icon, a title, a description and an action.
  • FormField
    Lays out a control with its Label, description and error, and links them for screen readers ("Email, text field, Enter a valid email").
  • Icon
    Every glyph: a Lucide icon at a Token size in a Colour Role, decorative unless labelled.
  • Input
    Single-line text field with Sizes, a leading icon, error/success status, a focus ring and an automatic Show/Hide password toggle.
  • InputOTP
    One-time code field: one hidden input drives N cells; numeric keyboard, SMS/Keychain autofill, paste, onComplete, error/success status and secure.
  • Label
    A field's visible label in the label type Variant, with a required marker read as "required".
  • List
    Settings-style rows: ListSection, ListSectionHeader, ListSectionFooter and ListItem (title, description, icon, trailing, chevron, destructive, onPress); FlashList-safe.
  • Progress
    A horizontal progress bar: a value 0–100 that animates, or indeterminate; role progressbar with its value.
  • RadioGroup
    RadioGroup and RadioGroupItem: a single choice from a list on the SelectionGroup Primitive, vertical or horizontal, with a haptic tick.
  • SchemeSwitcher
    Theme switcher / dark mode toggle: System, Light and Dark as SegmentedTabs, or one icon button cycling through them. Calls setScheme, which persists the choice.
  • SearchField
    Search box with a search icon, an automatic clear (×) button, a loading spinner and onSubmit from the keyboard's Search key.
  • SegmentedTabs
    In-screen tabs (not navigation): segmented or underline Variants, an animated indicator, and only the selected tab's content rendered.
  • Separator
    A hairline divider, horizontal or vertical, decorative by default.
  • Skeleton
    A placeholder shape shown while content loads; pulses unless Reduce Motion is on, hidden from screen readers.
  • Slider
    Pick a number in a range by dragging or tapping (Gesture Handler + Reanimated), snapped to steps with a haptic tick; adjustable for screen readers.
  • Spinner
    An indeterminate loading indicator in a Colour Role, announced as busy; still under Reduce Motion.
  • Switch
    A themed on/off Switch, identical on iOS and Android, with an optional label, a thumb that slides (instant under Reduce Motion) and a haptic tick.
  • TabNavigation
    The bottom tab bar: the custom tabBar of an Expo Router JS Tabs Layout, with classic and floating Variants, per-tab icons and badges from Tabs.Screen options.
  • Text
    All text: the type ramp as Variants, text Colour Roles, alignment and the font-scaling switch.
  • Textarea
    Multi-line text field that grows from minRows to maxRows, with a character counter when maxLength is set.
  • Toast
    Sonner-style Toasts: a <Toaster /> in the root Layout and toast(), toast.success/error/info/warning/dismiss. At most 3 stacked, 4s auto-dismiss, swipe away, announced, above native modals.
  • Profile header + grouped sections
    Drawer panel with a profile header (avatar, name, email, plan Badge), labelled sections (Main, Workspace, Support) and Log out pinned in the footer.
  • SaaS workspace
    Drawer panel for a SaaS workspace: logo and name with a close button, an Accent Colour onboarding item with a 5 Steps Badge, main items, a bottom group (Notifications with a count, Settings, Docs, Help) and a footer with a team switcher and the user's avatar.
  • Cover header
    Drawer panel with a dark cover (large avatar, name, View profile, a bell with a count, an overflow button and a round + action), icon items active in Accent Colour text, a divider and text-only secondary items.
  • Classic centred
    A centred sign-in Screen: logo, email and password, Forgot password, an "or" divider, then Apple and Google, and a Sign up link.
  • Social-first hero
    A social-first sign-in Screen: a dark brand hero (logo, headline, subtitle, centred) above a panel where Apple and Google lead and "Continue with email" reveals the email form.
  • Email-first, two steps
    An email-first sign-in Screen in two steps: the email, then the password, with "Email me a code instead" switching to a 6-digit code (InputOTP). Lucide chevron-left Back buttons.
  • Classic single form
    A classic sign-up Screen on one scroll: Apple and Google on top, "or", then name, email and password, a terms Checkbox with Terms and Privacy Policy links, Create account, and a Sign in link.
  • Step-by-step wizard
    A sign-up wizard, one question per step under a Progress bar: email, name, password, then a 6-digit code (InputOTP). Lucide chevron-left Back buttons; the Continue button rides above the keyboard.
  • Social-first + verify
    A brand-led, passwordless sign-up Screen: the logo and headline centred above Apple and Google; "Sign up with email" reveals the email field, and Send code moves on to a 6-digit code (InputOTP).
  • announce
    Reliable screen-reader announcements on iOS and Android (Button and Input status, Toast, error, success and warning Alerts).
  • FocusChain
    Automatic Next/Done between fields in on-screen order; Done submits the form.
  • FormField context
    Links a field to its label, description and error for screen readers; passes status, disabled and required down.
  • Keyboard
    KeyboardProvider setup, a scroll view that keeps the focused field above the keyboard, and a footer that keeps a form's main button above it.
  • Portal
    Renders content above every Screen (above native modals on iOS), layered and safe-area aware.
  • Pressable
    Pressed look from a Style Slot, tap area extended to 48, no handlers while disabled or loading, optional haptics, role and aria-* states.
  • SelectionGroup
    Single or multiple choice within a group, with radio, tab or checkbox roles and selected states.
  • useControllableState
    One implementation of controlled and uncontrolled values for form controls.
  • useMotion
    Enter/exit animations and timing/spring configs from the motion Tokens; instant when Reduce Motion is on.
  • Theme
    Tokens, Scale, Colour Roles, ThemeProvider, createStyles and the navigation theme.

Social-first + verify

A brand-led, passwordless sign-up Screen: the logo and headline centred above Apple and Google; "Sign up with email" reveals the email field, and Send code moves on to a 6-digit code (InputOTP).

View as Markdown
Blockauth
Style

Light

Dark

Your app is ready.

Inter body text, Inter headings.

smmdlgxl
you@example.com
Continue
Default Preset a0: Vega, neutral, neutral accent, default radius, Inter.Change Preset

Installation

$npx nativecn-cli@latest add sign-up-03 --route "(auth)/sign-up"
  • --route "(auth)/sign-up" is the Block's suggested route: change it to put the Screen somewhere else in your app folder.
  • In a project with the feature Structure, also pass --feature <name> (for example --feature auth).
  • add never overwrites an existing route file.

Also installs: theme, container, text, button, form-field, input, focus-chain, icon, input-otp, keyboard, toast.

Packages: react-hook-form@^7.89.0, zod@^4.6.5, @hookform/resolvers@^5.9.1, lucide-react-native@^1.51.0, react-native-svg.

What makes it different

Brand-led and passwordless; Apple/Google lead, and the email path sends an InputOTP code.

Block Variants

  • sign-up-01 – Classic single form: Social on top, then name, email, password and a terms Checkbox; one form.
  • sign-up-02 – Step-by-step wizard: Progress bar plus one question per screen: email → name → password → verify code.
  • sign-up-03 (this page) – Social-first + verify: Brand-led and passwordless; Apple/Google lead, and the email path sends an InputOTP code.

Props

PropType and description
onSocialSignIn(provider: "apple" | "google") => void | Promise<void>: run that provider's sign-up; a thrown error becomes a toast
onSendCode(email: string) => void | Promise<void>: email a 6-digit code (Send code, Resend); a thrown error becomes a toast and the email field stays
onSubmit(values: { email, code }) => void | Promise<void>: check the code and create the account; throw to show the error as a toast, resolve for an "Account created" toast

Accessibility

  • The headline and "Check your inbox" have role heading; the logo icon is decorative.
  • Moving to the code step puts screen-reader focus on "Check your inbox"; Back and Android's back button put it on the headline.
  • While a screen reader runs, the new step's field doesn't take the keyboard, so focus stays on the heading; without one, the field takes the keyboard as before.
  • Each field is in a FormField: its label is the field's accessible name, and a validation error becomes its hint and is announced once.
  • Errors from your callbacks and the success message are toast()s, which screen readers announce.
  • The submit Button is announced busy while your callback runs.
  • Apple and Google are labelled Buttons ("Continue with Apple", "Continue with Google"); their logos are decorative.
  • Back is an icon-only Button named "Back"; Android's back button steps back too.
  • The code is one InputOTP field, read as "Verification code, 6 digits".

Notes

Render it from a route: export default function Screen() { return <SignUp03 onSocialSignIn={signUpWith} onSendCode={sendCode} onSubmit={verify} />; }. There is no password: the email path checks the address with zod through react-hook-form, calls onSendCode, then asks for the 6-digit code. The step lives inside the Block; Back (an icon-only Button with a Lucide chevron-left) and Android's back button return to the email field, keeping it. The Create account button sits in a KeyboardStickyFooter above the keyboard. hooks/use-step-focus.ts moves screen-reader focus to the new step's heading on every step change; it is this Block's own copy. Validation errors appear under each field. Errors thrown by your callbacks, and the success message, appear only as toast()s: render <Toaster /> in the root Layout (init and create already do). The Apple and Google logos are bundled SVGs in components/social-buttons.tsx; the buttons only call onSocialSignIn. The Block never talks to a server: wire your auth backend in the route file through the props, and keep any session or token in expo-secure-store, never AsyncStorage.

Source

The Vega Style's code, exactly as add copies it into your app.

{screens}/sign-up-03/index.tsx

import { zodResolver } from "@hookform/resolvers/zod";
import { ChevronLeft, Hexagon } from "lucide-react-native";
import { useEffect, useRef, useState, type RefObject } from "react";
import { Controller, useForm, type UseFormReturn } from "react-hook-form";
import { BackHandler, View, type Text as RNText } from "react-native";
import { z } from "zod";

import { Button } from "@/components/button";
import { Container } from "@/components/container";
import { FormField } from "@/components/form-field";
import { Icon } from "@/components/icon";
import { Input } from "@/components/input";
import { InputOTP } from "@/components/input-otp";
import { FocusChain } from "@/components/primitives/focus-chain";
import { KeyboardStickyFooter } from "@/components/primitives/keyboard";
import { Text } from "@/components/text";
import { toast } from "@/components/toast";
import { createStyles, useTheme } from "@/theme";

import { SocialButtons, type SocialProvider } from "./components/social-buttons";
import { useStepFocus } from "./hooks/use-step-focus";

export type { SocialProvider } from "./components/social-buttons";

// sign-up-03, social first, then a code: brand-led and passwordless. Apple and Google are the
// main path; "Sign up with email" reveals the email field, and "Send code" moves on to a 6-digit
// code (InputOTP). There is no password. The step is kept inside the Block. Edit the copy, the
// logo and the rules below; they are yours.

/** `social`: the buttons (and, once revealed, the email field). `code`: the code check. */
type Step = "social" | "code";

const email = z.string().trim().min(1, "Enter your email").pipe(z.email("Enter a valid email"));
/** Each step checks only its own field; the other passes through. */
const schemas = {
  social: z.object({ email, code: z.string() }),
  code: z.object({ email, code: z.string().regex(/^\d{6}$/, "Enter the 6-digit code") }),
} satisfies Record<Step, z.ZodType<SignUp03Values>>;

/** The email and the code the user typed. */
export type SignUp03Values = { email: string; code: string };

export type SignUp03Props = {
  /** Run the provider's sign-up. Throw to show the error as a toast. */
  onSocialSignIn?: (provider: SocialProvider) => void | Promise<void>;
  /**
   * Email a 6-digit sign-up code (on "Send code" and on "Resend"). Throw to show the error as a
   * toast and stay on the email field.
   */
  onSendCode?: (email: string) => void | Promise<void>;
  /**
   * Check the code and create the account. Throw to show the error as a toast; resolve and an
   * "Account created" toast appears (navigate away here). Store any session in
   * expo-secure-store.
   */
  onSubmit?: (values: SignUp03Values) => void | Promise<void>;
};

export function SignUp03({ onSocialSignIn, onSendCode, onSubmit }: SignUp03Props) {
  const [step, setStep] = useState<Step>("social");
  const [emailOpen, setEmailOpen] = useState(false);
  const [sending, setSending] = useState(false);
  // Each step change moves the screen reader to the new heading (or, without one, the keyboard to
  // the new field).
  const { headingRef, autoFocus } = useStepFocus(step);
  const form = useForm<SignUp03Values>({
    resolver: zodResolver(schemas[step]),
    defaultValues: { email: "", code: "" },
  });
  const { handleSubmit, getValues, clearErrors } = form;

  const goTo = (next: Step) => {
    clearErrors();
    setStep(next);
  };

  // On the code step, Android's back button returns to the email field before it leaves.
  useEffect(() => {
    if (step !== "code") return;
    const subscription = BackHandler.addEventListener("hardwareBackPress", () => {
      clearErrors();
      setStep("social");
      return true;
    });
    return () => subscription.remove();
  }, [step, clearErrors]);

  /** Emails the code; says so in a toast. Returns whether it worked. */
  const sendCode = async (address: string) => {
    setSending(true);
    try {
      await onSendCode?.(address);
      toast.success("Code sent", { description: `Check ${address}` });
      return true;
    } catch (error) {
      toast.error("Couldn't send the code", { description: messageOf(error) });
      return false;
    } finally {
      setSending(false);
    }
  };

  const next = handleSubmit(async (values) => {
    if (step === "social") {
      if (await sendCode(values.email)) goTo("code");
      return;
    }
    try {
      await onSubmit?.(values);
      toast.success("Account created");
    } catch (error) {
      toast.error("Couldn't create your account", { description: messageOf(error) });
    }
  });

  const socialSignIn = async (provider: SocialProvider) => {
    try {
      await onSocialSignIn?.(provider);
    } catch (error) {
      toast.error("Couldn't sign up", { description: messageOf(error) });
    }
  };

  return step === "social" ? (
    <SocialStep
      form={form}
      headingRef={headingRef}
      autoFocus={autoFocus}
      emailOpen={emailOpen}
      onOpenEmail={() => setEmailOpen(true)}
      onSocialSignIn={socialSignIn}
      onNext={next}
    />
  ) : (
    <CodeStep
      form={form}
      headingRef={headingRef}
      autoFocus={autoFocus}
      email={getValues("email").trim()}
      sending={sending}
      onBack={() => goTo("social")}
      onResend={() => void sendCode(getValues("email").trim())}
      onNext={next}
    />
  );
}

type StepProps = {
  form: UseFormReturn<SignUp03Values>;
  /** On the step's heading, so a step change can move the screen reader to it. */
  headingRef: RefObject<RNText | null>;
  /** Whether the step's field takes the keyboard when it appears (off while a screen reader runs). */
  autoFocus: boolean;
  onNext: () => void;
};

/** The brand, Apple and Google, and the email field behind "Sign up with email". */
function SocialStep({
  form: {
    control,
    formState: { isSubmitting },
  },
  headingRef,
  autoFocus,
  emailOpen,
  onOpenEmail,
  onSocialSignIn,
  onNext,
}: StepProps & {
  emailOpen: boolean;
  onOpenEmail: () => void;
  onSocialSignIn: (provider: SocialProvider) => void;
}) {
  const styles = useStyles();
  // Opened here by "Sign up with email": the field takes focus. Already open when this step
  // appears (Back from the code): the heading gets the screen reader, so only take the keyboard
  // without one.
  const openedOnArrival = useRef(emailOpen).current;
  return (
    <Container contentContainerStyle={styles.socialContent}>
      <View style={styles.brand}>
        {/* Your logo goes here. */}
        <View style={styles.logo}>
          <Icon icon={Hexagon} size="lg" color="primaryForeground" />
        </View>
        <Text variant="h1" align="center" ref={headingRef}>
          Get started
        </Text>
        <Text variant="body" color="mutedForeground" align="center">
          Create your account in seconds.
        </Text>
      </View>

      <SocialButtons size="lg" onPress={onSocialSignIn} disabled={isSubmitting} />

      {emailOpen ? (
        <>
          <FocusChain onSubmit={onNext}>
            <Controller
              control={control}
              name="email"
              render={({ field, fieldState }) => (
                <FormField label="Email" error={fieldState.error?.message}>
                  <Input
                    ref={field.ref}
                    value={field.value}
                    onChangeText={field.onChange}
                    onBlur={field.onBlur}
                    autoFocus={autoFocus || !openedOnArrival}
                    placeholder="you@example.com"
                    keyboardType="email-address"
                    autoCapitalize="none"
                    autoComplete="email"
                    textContentType="emailAddress"
                  />
                </FormField>
              )}
            />
          </FocusChain>
          <Button size="lg" label="Send code" loading={isSubmitting} onPress={onNext} />
        </>
      ) : (
        <Button variant="ghost" size="lg" label="Sign up with email" onPress={onOpenEmail} />
      )}

      <Text variant="caption" color="mutedForeground" align="center">
        By continuing you agree to the Terms and Privacy Policy.
      </Text>
    </Container>
  );
}

/** The 6-digit code check, with Back and Resend. */
function CodeStep({
  form: {
    control,
    formState: { isSubmitting },
  },
  headingRef,
  autoFocus,
  email,
  sending,
  onBack,
  onResend,
  onNext,
}: StepProps & { email: string; sending: boolean; onBack: () => void; onResend: () => void }) {
  const styles = useStyles();
  const { controlHeight, spacing } = useTheme();
  return (
    <View style={styles.root}>
      <Container
        edges={["top"]}
        // Keep the code clear of the footer that rides above the keyboard.
        extraKeyboardSpace={controlHeight.lg + spacing[3] * 2}
        contentContainerStyle={styles.codeContent}
      >
        <Button
          variant="ghost"
          icon={ChevronLeft}
          aria-label="Back"
          onPress={onBack}
          style={styles.back}
        />

        <View style={styles.heading}>
          <Text variant="h1" ref={headingRef}>
            Check your inbox
          </Text>
          <Text variant="body" color="mutedForeground">
            {`Enter the 6-digit code sent to ${email}`}
          </Text>
        </View>

        <FocusChain onSubmit={onNext}>
          <Controller
            control={control}
            name="code"
            render={({ field, fieldState }) => (
              <FormField error={fieldState.error?.message}>
                <InputOTP
                  ref={field.ref}
                  value={field.value}
                  onChangeText={field.onChange}
                  onBlur={field.onBlur}
                  aria-label="Verification code"
                  autoFocus={autoFocus}
                  autoComplete="one-time-code"
                  textContentType="oneTimeCode"
                />
              </FormField>
            )}
          />
        </FocusChain>

        <View style={styles.resend}>
          <Text variant="small" color="mutedForeground">
            Didn&apos;t get it?
          </Text>
          <Button
            variant="link"
            size="sm"
            label="Resend"
            loading={sending}
            disabled={isSubmitting}
            onPress={onResend}
            style={styles.inline}
          />
        </View>
      </Container>

      <KeyboardStickyFooter style={styles.footer}>
        <Button
          size="lg"
          label="Create account"
          loading={isSubmitting}
          disabled={sending}
          onPress={onNext}
        />
      </KeyboardStickyFooter>
    </View>
  );
}

const useStyles = createStyles((t) => ({
  root: { flex: 1, backgroundColor: t.colors.background },
  socialContent: {
    gap: t.spacing[3],
    paddingHorizontal: t.spacing[6],
    paddingTop: t.spacing[10],
    paddingBottom: t.spacing[8],
  },
  // The brand fills the space above the buttons, centred.
  brand: {
    flexGrow: 1,
    alignItems: "center",
    justifyContent: "center",
    gap: t.spacing[3],
    paddingBottom: t.spacing[6],
  },
  logo: {
    width: t.scaleValue(72),
    height: t.scaleValue(72),
    marginBottom: t.spacing[2],
    alignItems: "center",
    justifyContent: "center",
    borderRadius: t.radius["2xl"],
    borderCurve: "continuous",
    backgroundColor: t.colors.primary,
  },
  codeContent: { paddingHorizontal: t.spacing[6], paddingTop: t.spacing[4] },
  // The chevron lines up with the content's edge; the tap area stays 48.
  back: { alignSelf: "flex-start", marginStart: -t.spacing[3] },
  heading: { gap: t.spacing[2], marginTop: t.spacing[4], marginBottom: t.spacing[4] },
  resend: {
    flexDirection: "row",
    flexWrap: "wrap",
    alignItems: "center",
    justifyContent: "center",
    gap: t.spacing[1],
  },
  inline: { paddingHorizontal: 0 },
  footer: { paddingHorizontal: t.spacing[6], backgroundColor: t.colors.background },
}));

/** The message to show for an error thrown by your callbacks. */
function messageOf(error: unknown): string {
  return error instanceof Error && error.message
    ? error.message
    : "Something went wrong. Try again.";
}

{screens}/sign-up-03/components/social-buttons.tsx

import type { LucideIcon, LucideProps } from "lucide-react-native";
import { forwardRef } from "react";
import { View } from "react-native";
import Svg, { Path } from "react-native-svg";

import { Button, type ButtonSize } from "@/components/button";
import { createStyles } from "@/theme";

export type SocialProvider = "apple" | "google";

// Brand logos, drawn to each brand's guidelines and shaped like a Lucide icon so Button can
// size and colour them. Apple's mark takes the text colour (black on light, white on dark);
// Google's "G" always keeps its four brand colours.

/** The Apple logo (Simple Icons, CC0), in the colour Button gives it. */
export const AppleLogo: LucideIcon = forwardRef<Svg, LucideProps>(function AppleLogo(
  { size = 24, color = "currentColor" },
  ref,
) {
  return (
    <Svg ref={ref} width={size} height={size} viewBox="0 0 24 24">
      <Path
        fill={color}
        d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"
      />
    </Svg>
  );
});

/** Google's four-colour "G" (its colours are part of the brand, so they never follow the Theme). */
export const GoogleLogo: LucideIcon = forwardRef<Svg, LucideProps>(function GoogleLogo(
  { size = 24 },
  ref,
) {
  return (
    <Svg ref={ref} width={size} height={size} viewBox="0 0 48 48">
      <Path
        fill="#EA4335"
        d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"
      />
      <Path
        fill="#4285F4"
        d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"
      />
      <Path
        fill="#FBBC05"
        d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"
      />
      <Path
        fill="#34A853"
        d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"
      />
    </Svg>
  );
});

const providers = [
  { provider: "apple", label: "Continue with Apple", icon: AppleLogo },
  { provider: "google", label: "Continue with Google", icon: GoogleLogo },
] as const satisfies { provider: SocialProvider; label: string; icon: LucideIcon }[];

const useStyles = createStyles((t) => ({
  root: { gap: t.spacing[3] },
}));

export type SocialButtonsProps = {
  /** Called with the provider; your code runs that provider's sign-in. */
  onPress: (provider: SocialProvider) => void;
  /** Default `md`. */
  size?: ButtonSize;
  disabled?: boolean;
};

/** "Continue with Apple" and "Continue with Google", as outline Buttons. */
export function SocialButtons({ onPress, size = "md", disabled = false }: SocialButtonsProps) {
  const styles = useStyles();
  return (
    <View style={styles.root}>
      {providers.map(({ provider, label, icon }) => (
        <Button
          key={provider}
          variant="outline"
          size={size}
          label={label}
          icon={icon}
          disabled={disabled}
          onPress={() => onPress(provider)}
        />
      ))}
    </View>
  );
}

{screens}/sign-up-03/hooks/use-step-focus.ts

import { useEffect, useRef, useState } from "react";
import { AccessibilityInfo, type Text } from "react-native";

// Private to this Block. Each multi-step Block (sign-in-03, sign-up-02, sign-up-03) keeps its own
// copy, so it installs on its own; if a second feature needs it, move it to {hooks} (ADR 0008).

/** Lets the new step reach the screen before the screen reader is pointed at it. */
const FOCUS_DELAY_MS = 150;

/**
 * Moves screen-reader focus (VoiceOver, TalkBack) to the step's heading every time `step`
 * changes, forward or back; not on the first render, where the Screen's own focus applies.
 *
 * Put `headingRef` on the step's heading (`<Text variant="h1" ref={headingRef}>`). Pass
 * `autoFocus` to the step's first field: it is `false` while a screen reader runs, so the
 * keyboard doesn't pull focus off the heading, and `true` otherwise.
 */
export function useStepFocus(step: string) {
  const headingRef = useRef<Text>(null);
  const screenReader = useScreenReaderEnabled();
  const shown = useRef(step);

  useEffect(() => {
    if (shown.current === step) return;
    shown.current = step;
    const timer = setTimeout(() => {
      const heading = headingRef.current;
      if (heading) AccessibilityInfo.sendAccessibilityEvent(heading, "focus");
    }, FOCUS_DELAY_MS);
    return () => clearTimeout(timer);
  }, [step]);

  return { headingRef, autoFocus: !screenReader };
}

/** Whether VoiceOver or TalkBack is running, kept up to date. */
function useScreenReaderEnabled(): boolean {
  const [enabled, setEnabled] = useState(false);
  useEffect(() => {
    let live = true;
    AccessibilityInfo.isScreenReaderEnabled().then(
      (on) => live && setEnabled(on),
      () => {},
    );
    const subscription = AccessibilityInfo.addEventListener("screenReaderChanged", setEnabled);
    return () => {
      live = false;
      subscription.remove();
    };
  }, []);
  return enabled;
}