# Login Sheet

A mobile app welcome screen with a Sign in with suiss bottom sheet for sharing or hiding an email.

Login Sheet is an open-source block from suiss/ui for React, React Native, SwiftUI and Jetpack Compose. The @esuiss/ui CLI copies its source into your project, so you own the code.

Source: https://ui.suiss.app/docs/blocks/login-sheet

**React**

**Example**

```tsx title="components/blocks/login-sheet.tsx"
"use client"

import { EnvelopeSimpleIcon, UserIcon, XIcon } from "@phosphor-icons/react"
import * as React from "react"

import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Spinner } from "@/components/ui/spinner"

export function LoginSheet() {
  const [open, setOpen] = React.useState(false)
  const [email, setEmail] = React.useState("hide")
  const [busy, setBusy] = React.useState(false)
  const [done, setDone] = React.useState(false)
  const closeRef = React.useRef<HTMLButtonElement>(null)
  const triggerRef = React.useRef<HTMLButtonElement>(null)
  const titleId = React.useId()

  const close = () => {
    setOpen(false)
    triggerRef.current?.focus({ preventScroll: true })
  }

  React.useEffect(() => {
    if (open) closeRef.current?.focus({ preventScroll: true })
  }, [open])

  React.useEffect(() => {
    if (!open) return
    const onKey = (event: KeyboardEvent) => {
      if (event.key === "Escape") close()
    }
    window.addEventListener("keydown", onKey)
    return () => window.removeEventListener("keydown", onKey)
  })

  const proceed = () => {
    setBusy(true)
    window.setTimeout(() => {
      setBusy(false)
      setOpen(false)
      setDone(true)
    }, 1000)
  }

  return (
    <section className="flex min-h-175 items-center justify-center bg-muted sm:px-6 sm:py-7">
      <div className="relative flex min-h-175 w-full flex-col overflow-hidden bg-background sm:h-160 sm:min-h-0 sm:w-85 sm:rounded-[2.75rem] sm:shadow-[0_0_0_10px_var(--color-foreground),0_0_0_11px_var(--color-muted-foreground),0_30px_70px_rgb(0_0_0/0.3)] dark:sm:shadow-[0_0_0_10px_var(--color-muted),0_0_0_11px_var(--color-input),0_30px_70px_rgb(0_0_0/0.5)]">
        <span
          aria-hidden
          className="absolute top-2.5 left-1/2 z-10 hidden h-7.5 w-27.5 -translate-x-1/2 rounded-full bg-black sm:block"
        />
        <div
          inert={open}
          className="flex flex-1 flex-col items-center px-7 pt-22.5 pb-8.5 text-center"
        >
          <span className="mb-5 size-21 rounded-[1.25rem] bg-linear-160 from-[oklch(0.75_0.15_150)] to-[oklch(0.55_0.15_180)]" />
          <h1 className="text-3xl font-semibold tracking-tight">
            {done ? "Welcome, Jamie" : "Trail Routes"}
          </h1>
          <p className="mt-2 text-base text-muted-foreground">
            {done
              ? "Your account was created with suiss."
              : "Save your routes and share them with friends."}
          </p>
          <div className="flex-1" />
          {done ? (
            <button
              type="button"
              onClick={() => setDone(false)}
              className="rounded-xs text-base text-primary outline-none hover:underline focus-visible:ring-3 focus-visible:ring-ring/50"
            >
              Sign out ›
            </button>
          ) : (
            <div className="flex w-full flex-col gap-2.5">
              <Button
                ref={triggerRef}
                size="xl"
                block
                onClick={() => setOpen(true)}
                className="h-12.5 bg-foreground text-lg font-semibold text-background hover:bg-foreground/90"
              >
                Sign in with suiss
              </Button>
              <Button
                size="xl"
                block
                variant="secondary"
                className="h-12.5 text-lg font-semibold"
              >
                Continue with email
              </Button>
            </div>
          )}
        </div>
        <div
          aria-hidden
          data-open={open ? "" : undefined}
          onClick={close}
          className="pointer-events-none absolute inset-0 bg-black/18 opacity-0 transition-opacity duration-300 data-open:pointer-events-auto data-open:opacity-100"
        />
        <div
          role="dialog"
          aria-modal
          aria-labelledby={titleId}
          inert={!open}
          data-open={open ? "" : undefined}
          className="absolute inset-x-0 bottom-0 flex translate-y-[105%] flex-col gap-3.5 rounded-t-3xl bg-popover px-5 pt-3.5 pb-7.5 transition-transform duration-500 ease-[cubic-bezier(0.3,1.15,0.5,1)] data-open:translate-y-0 motion-reduce:transition-none"
        >
          <div className="flex items-center">
            <span className="w-7" />
            <span className="flex-1 text-center text-lg font-bold tracking-tight">
              suiss
            </span>
            <Button
              ref={closeRef}
              variant="secondary"
              size="icon-sm"
              aria-label="Close"
              onClick={close}
              className="size-7 text-muted-foreground"
            >
              <XIcon className="size-3.5" />
            </Button>
          </div>
          <h2
            id={titleId}
            className="text-center text-2xl leading-tight font-semibold tracking-tight text-balance"
          >
            Use your suiss Account with Trail Routes
          </h2>
          <div className="rounded-xl bg-muted">
            <div className="flex items-center gap-3 px-3.5 py-2.5">
              <UserIcon className="size-5 text-muted-foreground" />
              <div className="flex flex-col text-start">
                <span className="text-xs text-muted-foreground">Name</span>
                <span className="text-base">Jamie Rivera</span>
              </div>
            </div>
            <div className="ms-11.5 h-px bg-border" />
            <RadioGroup
              value={email}
              onValueChange={(value) => setEmail(value as string)}
              aria-label="Email"
              className="gap-0"
            >
              {[
                {
                  value: "share",
                  label: "Share My Email",
                  hint: "jamie@example.com",
                },
                {
                  value: "hide",
                  label: "Hide My Email",
                  hint: "A unique address that forwards to you",
                },
              ].map((option, index) => (
                <Label
                  key={option.value}
                  className="flex cursor-pointer items-center gap-3 py-2.5 pe-3.5 data-[index='0']:ps-3.5 data-[index='1']:ms-11.5 data-[index='1']:border-t data-[index='1']:border-border"
                  data-index={index}
                >
                  {index === 0 && (
                    <EnvelopeSimpleIcon
                      className="size-5 text-muted-foreground"
                    />
                  )}
                  <span className="flex flex-1 flex-col text-start">
                    <span className="text-base">{option.label}</span>
                    <span className="text-xs text-muted-foreground">
                      {option.hint}
                    </span>
                  </span>
                  <RadioGroupItem value={option.value} className="size-5.5" />
                </Label>
              ))}
            </RadioGroup>
          </div>
          <Button
            size="xl"
            block
            onClick={proceed}
            disabled={busy}
            className="h-12.5 text-lg font-semibold"
          >
            {busy ? <Spinner className="size-5" /> : "Continue with face scan"}
          </Button>
        </div>
      </div>
    </section>
  )
}
```

A mobile app welcome screen whose Sign in with suiss button slides up a bottom sheet, where users choose to share or hide their email before continuing. Completing the flow greets the user with a Sign out link. From small tablets up the screen sits in a phone frame; on the smallest screens it fills the section. Use it for third-party sign-in or any consent step shown as a bottom sheet.

## Installation

### CLI

```bash title="Terminal"
npx shadcn@latest add @esuiss/login-sheet
```

Works in any project with a `components.json`. See [Installation](https://ui.suiss.app/docs/installation#add-components) for the one-line registry setup. Or use the suiss/ui CLI:

```bash title="Terminal"
npx @esuiss/ui@latest add blocks login-sheet
```

### Manual

1. Install the dependencies:

```bash title="Terminal"
npm install @phosphor-icons/react
```

2. Add the pieces it builds on: [Button](https://ui.suiss.app/docs/components/button), [Label](https://ui.suiss.app/docs/components/label), [Radio Group](https://ui.suiss.app/docs/components/radio-group), [Spinner](https://ui.suiss.app/docs/components/spinner).

3. Copy the source into `components/blocks/login-sheet.tsx`:

```tsx title="components/blocks/login-sheet.tsx"
"use client"

import { EnvelopeSimpleIcon, UserIcon, XIcon } from "@phosphor-icons/react"
import * as React from "react"

import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Spinner } from "@/components/ui/spinner"

export function LoginSheet() {
  const [open, setOpen] = React.useState(false)
  const [email, setEmail] = React.useState("hide")
  const [busy, setBusy] = React.useState(false)
  const [done, setDone] = React.useState(false)
  const closeRef = React.useRef<HTMLButtonElement>(null)
  const triggerRef = React.useRef<HTMLButtonElement>(null)
  const titleId = React.useId()

  const close = () => {
    setOpen(false)
    triggerRef.current?.focus({ preventScroll: true })
  }

  React.useEffect(() => {
    if (open) closeRef.current?.focus({ preventScroll: true })
  }, [open])

  React.useEffect(() => {
    if (!open) return
    const onKey = (event: KeyboardEvent) => {
      if (event.key === "Escape") close()
    }
    window.addEventListener("keydown", onKey)
    return () => window.removeEventListener("keydown", onKey)
  })

  const proceed = () => {
    setBusy(true)
    window.setTimeout(() => {
      setBusy(false)
      setOpen(false)
      setDone(true)
    }, 1000)
  }

  return (
    <section className="flex min-h-175 items-center justify-center bg-muted sm:px-6 sm:py-7">
      <div className="relative flex min-h-175 w-full flex-col overflow-hidden bg-background sm:h-160 sm:min-h-0 sm:w-85 sm:rounded-[2.75rem] sm:shadow-[0_0_0_10px_var(--color-foreground),0_0_0_11px_var(--color-muted-foreground),0_30px_70px_rgb(0_0_0/0.3)] dark:sm:shadow-[0_0_0_10px_var(--color-muted),0_0_0_11px_var(--color-input),0_30px_70px_rgb(0_0_0/0.5)]">
        <span
          aria-hidden
          className="absolute top-2.5 left-1/2 z-10 hidden h-7.5 w-27.5 -translate-x-1/2 rounded-full bg-black sm:block"
        />
        <div
          inert={open}
          className="flex flex-1 flex-col items-center px-7 pt-22.5 pb-8.5 text-center"
        >
          <span className="mb-5 size-21 rounded-[1.25rem] bg-linear-160 from-[oklch(0.75_0.15_150)] to-[oklch(0.55_0.15_180)]" />
          <h1 className="text-3xl font-semibold tracking-tight">
            {done ? "Welcome, Jamie" : "Trail Routes"}
          </h1>
          <p className="mt-2 text-base text-muted-foreground">
            {done
              ? "Your account was created with suiss."
              : "Save your routes and share them with friends."}
          </p>
          <div className="flex-1" />
          {done ? (
            <button
              type="button"
              onClick={() => setDone(false)}
              className="rounded-xs text-base text-primary outline-none hover:underline focus-visible:ring-3 focus-visible:ring-ring/50"
            >
              Sign out ›
            </button>
          ) : (
            <div className="flex w-full flex-col gap-2.5">
              <Button
                ref={triggerRef}
                size="xl"
                block
                onClick={() => setOpen(true)}
                className="h-12.5 bg-foreground text-lg font-semibold text-background hover:bg-foreground/90"
              >
                Sign in with suiss
              </Button>
              <Button
                size="xl"
                block
                variant="secondary"
                className="h-12.5 text-lg font-semibold"
              >
                Continue with email
              </Button>
            </div>
          )}
        </div>
        <div
          aria-hidden
          data-open={open ? "" : undefined}
          onClick={close}
          className="pointer-events-none absolute inset-0 bg-black/18 opacity-0 transition-opacity duration-300 data-open:pointer-events-auto data-open:opacity-100"
        />
        <div
          role="dialog"
          aria-modal
          aria-labelledby={titleId}
          inert={!open}
          data-open={open ? "" : undefined}
          className="absolute inset-x-0 bottom-0 flex translate-y-[105%] flex-col gap-3.5 rounded-t-3xl bg-popover px-5 pt-3.5 pb-7.5 transition-transform duration-500 ease-[cubic-bezier(0.3,1.15,0.5,1)] data-open:translate-y-0 motion-reduce:transition-none"
        >
          <div className="flex items-center">
            <span className="w-7" />
            <span className="flex-1 text-center text-lg font-bold tracking-tight">
              suiss
            </span>
            <Button
              ref={closeRef}
              variant="secondary"
              size="icon-sm"
              aria-label="Close"
              onClick={close}
              className="size-7 text-muted-foreground"
            >
              <XIcon className="size-3.5" />
            </Button>
          </div>
          <h2
            id={titleId}
            className="text-center text-2xl leading-tight font-semibold tracking-tight text-balance"
          >
            Use your suiss Account with Trail Routes
          </h2>
          <div className="rounded-xl bg-muted">
            <div className="flex items-center gap-3 px-3.5 py-2.5">
              <UserIcon className="size-5 text-muted-foreground" />
              <div className="flex flex-col text-start">
                <span className="text-xs text-muted-foreground">Name</span>
                <span className="text-base">Jamie Rivera</span>
              </div>
            </div>
            <div className="ms-11.5 h-px bg-border" />
            <RadioGroup
              value={email}
              onValueChange={(value) => setEmail(value as string)}
              aria-label="Email"
              className="gap-0"
            >
              {[
                {
                  value: "share",
                  label: "Share My Email",
                  hint: "jamie@example.com",
                },
                {
                  value: "hide",
                  label: "Hide My Email",
                  hint: "A unique address that forwards to you",
                },
              ].map((option, index) => (
                <Label
                  key={option.value}
                  className="flex cursor-pointer items-center gap-3 py-2.5 pe-3.5 data-[index='0']:ps-3.5 data-[index='1']:ms-11.5 data-[index='1']:border-t data-[index='1']:border-border"
                  data-index={index}
                >
                  {index === 0 && (
                    <EnvelopeSimpleIcon
                      className="size-5 text-muted-foreground"
                    />
                  )}
                  <span className="flex flex-1 flex-col text-start">
                    <span className="text-base">{option.label}</span>
                    <span className="text-xs text-muted-foreground">
                      {option.hint}
                    </span>
                  </span>
                  <RadioGroupItem value={option.value} className="size-5.5" />
                </Label>
              ))}
            </RadioGroup>
          </div>
          <Button
            size="xl"
            block
            onClick={proceed}
            disabled={busy}
            className="h-12.5 text-lg font-semibold"
          >
            {busy ? <Spinner className="size-5" /> : "Continue with face scan"}
          </Button>
        </div>
      </div>
    </section>
  )
}
```

4. Update the import paths to match your project.

## Usage

```tsx
import { LoginSheet } from "@/components/blocks/login-sheet"
```

```tsx
<LoginSheet />
```

Replace `proceed`, which fakes a one-second request, with your identity provider's sign-in, and send the chosen email option along. Update the Trail Routes app name, tagline and account details, and give Continue with email a handler.

## Notes

The sheet behaves as a modal dialog: focus moves into it, Escape or a click on the scrim closes it, and focus returns to the trigger.

**React Native, SwiftUI, Jetpack Compose**

![Preview](https://ui.suiss.app/previews/blocks-login-sheet--default.light.webp)

```tsx title="uniwind/components/blocks/login-sheet.tsx"
import { cn } from "cn"
import { EnvelopeSimpleIcon, UserIcon, XIcon } from "phosphor-react-native"
import * as React from "react"
import { Pressable, View } from "react-native"
import { Button } from "@/components/ui/button"
import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerTitle,
} from "@/components/ui/drawer"
import { Icon } from "@/components/ui/icon"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Screen } from "@/components/ui/screen"
import { Spinner } from "@/components/ui/spinner"
import { Text } from "@/components/ui/text"

const EMAIL_OPTIONS: {
  value: "share" | "hide"
  label: string
  hint: string
}[] = [
  { value: "share", label: "Share My Email", hint: "jamie@example.com" },
  {
    value: "hide",
    label: "Hide My Email",
    hint: "A unique address that forwards to you",
  },
]

function LoginSheet({
  onContinueWithEmail,
  onContinue,
}: {
  onContinueWithEmail?: () => void
  onContinue?: (email: "share" | "hide") => void
}) {
  const [open, setOpen] = React.useState(false)
  const [email, setEmail] = React.useState<"share" | "hide">("hide")
  const [busy, setBusy] = React.useState(false)
  const [done, setDone] = React.useState(false)

  React.useEffect(() => {
    if (!busy) return
    const timer = setTimeout(() => {
      setBusy(false)
      setOpen(false)
      setDone(true)
    }, 1000)
    return () => clearTimeout(timer)
  }, [busy])

  return (
    <Screen>
      <View className="flex-1 items-center px-7 pt-22.5 pb-8.5">
        <View className="mb-5 size-21 rounded-[1.25rem] bg-green-500" />
        <Text role="heading" className="text-center text-3xl font-semibold">
          {done ? "Welcome, Jamie" : "Trail Routes"}
        </Text>
        <Text className="mt-2 text-center text-base text-muted-foreground">
          {done
            ? "Your account was created with suiss."
            : "Save your routes and share them with friends."}
        </Text>
        <View className="flex-1" />
        {done ? (
          <Pressable
            role="link"
            hitSlop={8}
            onPress={() => setDone(false)}
            className="active:opacity-60"
          >
            <Text className="text-base text-primary">Sign out ›</Text>
          </Pressable>
        ) : (
          <View className="w-full gap-2.5">
            <Button
              size="xl"
              block
              onPress={() => setOpen(true)}
              className="h-12.5 bg-foreground active:opacity-80"
            >
              <Text className="text-lg font-semibold text-background">
                Sign in with suiss
              </Text>
            </Button>
            <Button
              size="xl"
              block
              variant="secondary"
              onPress={onContinueWithEmail}
              className="h-12.5"
            >
              <Text className="text-lg font-semibold">Continue with email</Text>
            </Button>
          </View>
        )}
      </View>
      <Drawer open={open} onOpenChange={setOpen}>
        <DrawerContent className="gap-3.5 px-5 pt-3.5 pb-4">
          <View className="flex-row items-center">
            <View className="w-7" />
            <Text className="flex-1 text-center text-lg font-bold">suiss</Text>
            <DrawerClose
              render={
                <Button
                  variant="secondary"
                  size="icon-sm"
                  className="size-7"
                  accessibilityLabel="Close"
                />
              }
            >
              <Icon
                as={XIcon}
                weight="bold"
                className="size-3.5 text-muted-foreground"
              />
            </DrawerClose>
          </View>
          <DrawerTitle className="text-2xl font-semibold">
            Use your suiss Account with Trail Routes
          </DrawerTitle>
          <View className="rounded-xl bg-muted">
            <View className="flex-row items-center gap-3 px-3.5 py-2.5">
              <Icon as={UserIcon} className="size-5 text-muted-foreground" />
              <View>
                <Text className="text-xs text-muted-foreground">Name</Text>
                <Text className="text-base">Jamie Rivera</Text>
              </View>
            </View>
            <View className="ms-11.5 h-px bg-border" />
            <RadioGroup
              value={email}
              onValueChange={setEmail}
              accessibilityLabel="Email"
              className="gap-0"
            >
              {EMAIL_OPTIONS.map((option, index) => (
                <Pressable
                  key={option.value}
                  onPress={() => setEmail(option.value)}
                  className={cn(
                    "flex-row items-center gap-3 py-2.5 pe-3.5",
                    index === 0 ? "ps-3.5" : "ms-11.5 border-t border-border"
                  )}
                >
                  {index === 0 ? (
                    <Icon
                      as={EnvelopeSimpleIcon}
                      className="size-5 text-muted-foreground"
                    />
                  ) : null}
                  <View className="flex-1">
                    <Text className="text-base">{option.label}</Text>
                    <Text className="text-xs text-muted-foreground">
                      {option.hint}
                    </Text>
                  </View>
                  <RadioGroupItem
                    value={option.value}
                    accessibilityLabel={option.label}
                    className="size-5.5"
                  />
                </Pressable>
              ))}
            </RadioGroup>
          </View>
          <Button
            size="xl"
            block
            onPress={() => {
              onContinue?.(email)
              setBusy(true)
            }}
            disabled={busy}
            className="h-12.5"
          >
            {busy ? (
              <Spinner size={20} colorClassName="accent-primary-foreground" />
            ) : (
              <Text className="text-lg font-semibold">
                Continue with face scan
              </Text>
            )}
          </Button>
        </DrawerContent>
      </Drawer>
    </Screen>
  )
}

export { LoginSheet }
```

```swift title="swift/Blocks/LoginSheet.swift"
import PhosphorSwift
import SwiftUI

public struct LoginSheet: View {
  private var onContinueWithEmail: (() -> Void)?
  private var onContinue: ((Bool) -> Void)?
  @State private var open: Bool
  @State private var done: Bool
  @Environment(\.accessibilityReduceMotion) private var reduceMotion

  public init(onContinue: ((_ shareEmail: Bool) -> Void)? = nil, onContinueWithEmail: (() -> Void)? = nil) {
    self.init(open: false, done: false)
    self.onContinue = onContinue
    self.onContinueWithEmail = onContinueWithEmail
  }

  init(open: Bool, done: Bool) {
    _open = State(initialValue: open)
    _done = State(initialValue: done)
  }

  public var body: some View {
    Screen {
      VStack(spacing: 0) {
        Palette.green500
          .frame(width: 84, height: 84)
          .clipShape(RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous))
          .padding(.bottom, 20)
          .accessibilityHidden(true)
        Text(done ? "Welcome, Jamie" : "Trail Routes")
          .scaledTextStyle(.xl2, weight: .bold)
        Text(done ? "Your account was created with suiss." : "Save your routes and share them with friends.")
          .scaledTextStyle(.base)
          .foregroundStyle(EsuissColor.mutedForeground)
          .padding(.top, 8)
      }
      .multilineTextAlignment(.center)
      .fixedSize(horizontal: false, vertical: true)
      .padding(.horizontal, 28)
      .padding(.top, 90)
      .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
      BottomBar(separator: false) {
        BottomBarContent {
          if done {
            Button("Sign out ›") { done = false }
              .buttonStyle(LoginSheetLinkStyle())
              .padding(.bottom, 12)
          } else {
            Button("Sign in with suiss") { open = true }
              .buttonStyle(LoginSheetLabelStyle())
            Button("Continue with email") { onContinueWithEmail?() }
              .buttonStyle(.variant(.secondary, size: .xl, block: true))
          }
        }
        .padding(.bottom, 12)
      }
    }
    .animation(reduceMotion ? nil : .easeInOut(duration: 0.3), value: done)
    .drawer(isPresented: $open) {
      LoginSheetPanel(onContinue: onContinue) {
        open = false
        done = true
      }
    }
  }
}

struct LoginSheetPanel: View {
  var onContinue: ((Bool) -> Void)?
  let onComplete: () -> Void

  @State private var email = "hide"
  @State private var busy = false
  @State private var task: Task<Void, Never>?
  @Environment(\.overlayDismiss) private var dismiss

  var body: some View {
    VStack(spacing: 14) {
      HStack {
        Color.clear.frame(width: 28, height: 28)
        Text(verbatim: "suiss")
          .scaledFont(.lg, weight: .bold)
          .frame(maxWidth: .infinity)
        Button {
          dismiss?()
        } label: {
          Ph.x.bold.frame(width: 14, height: 14)
        }
        .buttonStyle(.variant(.secondary, size: .iconSm))
        .foregroundStyle(EsuissColor.mutedForeground)
        .accessibilityLabel(Text("Close"))
      }
      Text("Use your suiss Account with Trail Routes")
        .scaledTextStyle(.xl, weight: .bold)
        .multilineTextAlignment(.center)
        .fixedSize(horizontal: false, vertical: true)
        .accessibilityAddTraits(.isHeader)
      VStack(spacing: 0) {
        HStack(spacing: 12) {
          Ph.user.regular
            .frame(width: 20, height: 20)
            .foregroundStyle(EsuissColor.mutedForeground)
          VStack(alignment: .leading, spacing: 0) {
            Text("Name")
              .scaledFont(.xs)
              .foregroundStyle(EsuissColor.mutedForeground)
            Text(verbatim: "Jamie Rivera")
              .scaledFont(.base)
          }
          Spacer(minLength: 0)
        }
        .padding(.horizontal, 14)
        .padding(.vertical, 10)
        .accessibilityElement(children: .combine)
        EsuissColor.border.frame(height: 1).padding(.leading, 46)
        RadioGroup(selection: $email) {
          VStack(spacing: 0) {
            option("share", label: "Share My Email", hint: Text(verbatim: "jamie@example.com"), icon: true)
            EsuissColor.border.frame(height: 1).padding(.leading, 46)
            option("hide", label: "Hide My Email", hint: Text("A unique address that forwards to you"), icon: false)
          }
        }
      }
      .background(EsuissColor.muted, in: RoundedRectangle(cornerRadius: Radius.xl, style: .continuous))
      Button {
        onContinue?(email == "share")
        busy = true
        task?.cancel()
        task = Task {
          guard (try? await Task.sleep(for: .milliseconds(1000))) != nil else { return }
          busy = false
          onComplete()
        }
      } label: {
        if busy {
          Spinner(dimension: 20)
        } else {
          Text("Continue with face scan")
        }
      }
      .buttonStyle(.variant(.default, size: .xl, block: true))
      .disabled(busy)
    }
    .foregroundStyle(EsuissColor.foreground)
    .padding(.horizontal, 20)
    .padding(.top, 14)
    .padding(.bottom, 16)
    .background(EsuissColor.popover)
    .onDisappear { task?.cancel() }
  }

  private func option(_ value: String, label: LocalizedStringKey, hint: Text, icon: Bool) -> some View {
    HStack(spacing: 12) {
      if icon {
        Ph.envelopeSimple.regular
          .frame(width: 20, height: 20)
          .foregroundStyle(EsuissColor.mutedForeground)
      }
      VStack(alignment: .leading, spacing: 0) {
        Text(label)
          .scaledFont(.base)
        hint
          .scaledFont(.xs)
          .foregroundStyle(EsuissColor.mutedForeground)
      }
      .frame(maxWidth: .infinity, alignment: .leading)
      RadioGroupItem(value: value)
    }
    .padding(.leading, icon ? 14 : 46)
    .padding(.trailing, 14)
    .padding(.vertical, 10)
    .contentShape(Rectangle())
    .onTapGesture { email = value }
    .accessibilityElement(children: .combine)
    .accessibilityAddTraits(email == value ? [.isButton, .isSelected] : .isButton)
  }
}

private struct LoginSheetLabelStyle: ButtonStyle {
  func makeBody(configuration: Configuration) -> some View {
    configuration.label
      .scaledFont(.lg, weight: .semibold)
      .foregroundStyle(EsuissColor.background)
      .frame(maxWidth: .infinity, minHeight: 56)
      .background(EsuissColor.foreground, in: RoundedRectangle(cornerRadius: Radius.xl, style: .continuous))
      .opacity(configuration.isPressed ? 0.8 : 1)
      .contentShape(Rectangle())
  }
}

private struct LoginSheetLinkStyle: ButtonStyle {
  func makeBody(configuration: Configuration) -> some View {
    configuration.label
      .scaledFont(.base)
      .foregroundStyle(EsuissColor.primary)
      .opacity(configuration.isPressed ? 0.8 : 1)
      .frame(minHeight: 44)
      .contentShape(Rectangle())
  }
}
```

```kt title="kotlin/blocks/LoginSheet.kt"
package com.esuiss.ui.blocks

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.EnvelopeSimple
import com.adamglin.phosphoricons.regular.User
import com.esuiss.ui.components.BottomBar
import com.esuiss.ui.components.BottomBarContent
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonSize
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Drawer
import com.esuiss.ui.components.DrawerFooter
import com.esuiss.ui.components.DrawerHeader
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.RadioGroup
import com.esuiss.ui.components.RadioGroupItem
import com.esuiss.ui.components.Screen
import com.esuiss.ui.components.Spinner
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Palette
import com.esuiss.ui.theme.Radius
import com.esuiss.ui.theme.Typography
import kotlinx.coroutines.delay

private class EmailChoice(val value: String, val label: String, val hint: String)

private val EmailChoices = listOf(
    EmailChoice("share", "Share My Email", "jamie@example.com"),
    EmailChoice("hide", "Hide My Email", "A unique address that forwards to you"),
)

@Composable
fun LoginSheet(
    modifier: Modifier = Modifier,
    onContinue: (email: String) -> Unit = {},
    onContinueWithEmail: () -> Unit = {},
) {
    val colors = EsuissTheme.colors
    var open by rememberSaveable { mutableStateOf(false) }
    var email by rememberSaveable { mutableStateOf("hide") }
    var busy by remember { mutableStateOf(false) }
    var done by rememberSaveable { mutableStateOf(false) }

    LaunchedEffect(busy) {
        if (!busy) return@LaunchedEffect
        delay(1000)
        busy = false
        open = false
        done = true
    }

    Screen(modifier) {
        Column(Modifier.fillMaxSize()) {
            Column(
                modifier = Modifier
                    .weight(1f)
                    .fillMaxWidth()
                    .padding(horizontal = 28.dp),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally,
            ) {
                Box(
                    Modifier
                        .padding(bottom = 20.dp)
                        .size(84.dp)
                        .clip(RoundedCornerShape(20.dp))
                        .background(Palette.green500),
                )
                BasicText(
                    if (done) "Welcome, Jamie" else "Trail Routes",
                    modifier = Modifier.semantics { heading() },
                    style = Typography.xl3.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold, textAlign = TextAlign.Center),
                )
                Spacer(Modifier.height(8.dp))
                BasicText(
                    if (done) "Your account was created with suiss." else "Save your routes and share them with friends.",
                    style = Typography.base.copy(color = colors.mutedForeground, textAlign = TextAlign.Center),
                )
            }
            BottomBar {
                BottomBarContent {
                    if (done) {
                        Button("Sign out ›", { done = false }, variant = ButtonVariant.Plain)
                    } else {
                        InvertedButton("Sign in with suiss") { open = true }
                        Button(
                            onClick = onContinueWithEmail,
                            variant = ButtonVariant.Secondary,
                            size = ButtonSize.Xl,
                            block = true,
                        ) {
                            BasicText(
                                "Continue with email",
                                style = Typography.lg.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold),
                            )
                        }
                    }
                }
            }
        }
        Drawer(
            open = open,
            onOpenChange = { if (!busy) open = it },
            showSwipeHandle = true,
            showCloseButton = true,
            closeLabel = null,
        ) {
            DrawerHeader {
                BasicText(
                    "suiss",
                    style = Typography.lg.copy(color = colors.foreground, fontWeight = FontWeight.Bold, textAlign = TextAlign.Center),
                )
            }
            Column(
                modifier = Modifier.padding(start = 20.dp, end = 20.dp, top = 14.dp, bottom = 14.dp),
                verticalArrangement = Arrangement.spacedBy(14.dp),
            ) {
                BasicText(
                    "Use your suiss Account with Trail Routes",
                    modifier = Modifier
                        .fillMaxWidth()
                        .semantics { heading() },
                    style = Typography.xl2.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold, textAlign = TextAlign.Center),
                )
                val shape = RoundedCornerShape(Radius.xl)
                Column(
                    Modifier
                        .fillMaxWidth()
                        .clip(shape)
                        .background(colors.muted, shape),
                ) {
                    InfoRow(PhosphorIcons.Regular.User, "Name", "Jamie Rivera")
                    Divider()
                    RadioGroup(value = email, onValueChange = { email = it }) {
                        Column {
                            EmailChoices.forEachIndexed { index, choice ->
                                if (index > 0) Divider()
                                ChoiceRow(choice, icon = if (index == 0) PhosphorIcons.Regular.EnvelopeSimple else null, selected = email == choice.value) {
                                    email = choice.value
                                }
                            }
                        }
                    }
                }
            }
            DrawerFooter {
                Button(
                    onClick = {
                        onContinue(email)
                        busy = true
                    },
                    size = ButtonSize.Xl,
                    block = true,
                    enabled = !busy,
                ) {
                    if (busy) {
                        Spinner(size = 20.dp, color = colors.primaryForeground)
                    } else {
                        BasicText(
                            "Continue with face scan",
                            style = Typography.lg.copy(color = colors.primaryForeground, fontWeight = FontWeight.SemiBold),
                        )
                    }
                }
            }
        }
    }
}

@Composable
private fun InvertedButton(text: String, onClick: () -> Unit) {
    val colors = EsuissTheme.colors
    val interaction = remember { MutableInteractionSource() }
    val pressed by interaction.collectIsPressedAsState()
    val shape = RoundedCornerShape(Radius.xl)
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(56.dp)
            .clip(shape)
            .alpha(if (pressed) 0.8f else 1f)
            .background(colors.foreground, shape)
            .clickable(interactionSource = interaction, indication = null, role = Role.Button, onClick = onClick),
        contentAlignment = Alignment.Center,
    ) {
        BasicText(text, style = Typography.lg.copy(color = colors.background, fontWeight = FontWeight.SemiBold))
    }
}

@Composable
private fun Divider() {
    Box(
        Modifier
            .padding(start = 46.dp)
            .fillMaxWidth()
            .height(1.dp)
            .background(EsuissTheme.colors.border),
    )
}

@Composable
private fun InfoRow(icon: ImageVector, caption: String, value: String) {
    val colors = EsuissTheme.colors
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 14.dp, vertical = 10.dp),
        horizontalArrangement = Arrangement.spacedBy(12.dp),
        verticalAlignment = Alignment.CenterVertically,
    ) {
        Icon(icon, contentDescription = null, size = 20.dp, tint = colors.mutedForeground)
        Column {
            BasicText(caption, style = Typography.xs.copy(color = colors.mutedForeground))
            BasicText(value, style = Typography.base.copy(color = colors.foreground))
        }
    }
}

@Composable
private fun ChoiceRow(choice: EmailChoice, icon: ImageVector?, selected: Boolean, onSelect: () -> Unit) {
    val colors = EsuissTheme.colors
    val interaction = remember { MutableInteractionSource() }
    val pressed by interaction.collectIsPressedAsState()
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .heightIn(min = 52.dp)
            .alpha(if (pressed) 0.8f else 1f)
            .selectable(
                selected = selected,
                interactionSource = interaction,
                indication = null,
                role = Role.RadioButton,
                onClick = onSelect,
            )
            .padding(start = if (icon == null) 46.dp else 14.dp, end = 14.dp, top = 10.dp, bottom = 10.dp),
        horizontalArrangement = Arrangement.spacedBy(12.dp),
        verticalAlignment = Alignment.CenterVertically,
    ) {
        if (icon != null) Icon(icon, contentDescription = null, size = 20.dp, tint = colors.mutedForeground)
        Column(Modifier.weight(1f)) {
            BasicText(choice.label, style = Typography.base.copy(color = colors.foreground))
            BasicText(choice.hint, style = Typography.xs.copy(color = colors.mutedForeground))
        }
        Box(Modifier.graphicsLayer { scaleX = 1.3f; scaleY = 1.3f }) {
            RadioGroupItem(choice.value)
        }
    }
}
```

An app welcome screen with two buttons: Sign in with suiss and Continue with email. The first opens a `Drawer` that shows the account name and a `RadioGroup` to share the real email or use a hidden forwarding address, with a Continue `Button` that shows a `Spinner` before closing the sheet and showing a welcome message.

## Installation

**React Native**

```bash title="Terminal"
npx shadcn@latest add @esuiss-rn/login-sheet
```

See [Installation](https://ui.suiss.app/docs/installation) for the `components.json` setup. Or use the suiss/ui CLI:

```bash title="Terminal"
npx @esuiss/ui@latest add login-sheet
```

**SwiftUI**

```bash title="Terminal"
npx @esuiss/ui@latest add login-sheet --platform swift
```

**Jetpack Compose**

```bash title="Terminal"
npx @esuiss/ui@latest add login-sheet --platform kotlin
```

The CLI also adds what it builds on: [Button](https://ui.suiss.app/docs/components/button), [Drawer](https://ui.suiss.app/docs/components/drawer), [Icon](https://ui.suiss.app/docs/components/icon), [Radio Group](https://ui.suiss.app/docs/components/radio-group), [Screen](https://ui.suiss.app/docs/components/screen), [Spinner](https://ui.suiss.app/docs/components/spinner), [Text](https://ui.suiss.app/docs/components/text).

### Manual

**React Native**

**Uniwind**

1. Install the dependencies:

```bash title="Terminal"
npx expo install cn phosphor-react-native react-native-svg
```

2. Add the pieces it builds on: [Button](https://ui.suiss.app/docs/components/button), [Drawer](https://ui.suiss.app/docs/components/drawer), [Icon](https://ui.suiss.app/docs/components/icon), [Radio Group](https://ui.suiss.app/docs/components/radio-group), [Screen](https://ui.suiss.app/docs/components/screen), [Spinner](https://ui.suiss.app/docs/components/spinner), [Text](https://ui.suiss.app/docs/components/text).

3. Copy the source into `components/blocks/login-sheet.tsx`:

- `uniwind/components/blocks/login-sheet.tsx`: shown above

4. Update the import paths to match your project.

**NativeWind**

1. Install the dependencies:

```bash title="Terminal"
npx expo install cn phosphor-react-native react-native-svg
```

2. Add the pieces it builds on: [Button](https://ui.suiss.app/docs/components/button), [Drawer](https://ui.suiss.app/docs/components/drawer), [Icon](https://ui.suiss.app/docs/components/icon), [Radio Group](https://ui.suiss.app/docs/components/radio-group), [Screen](https://ui.suiss.app/docs/components/screen), [Spinner](https://ui.suiss.app/docs/components/spinner), [Text](https://ui.suiss.app/docs/components/text).

3. Copy the source into `components/blocks/login-sheet.tsx`:

```tsx title="nativewind/components/blocks/login-sheet.tsx"
import { cn } from "cn"
import { EnvelopeSimpleIcon, UserIcon, XIcon } from "phosphor-react-native"
import * as React from "react"
import { Pressable, View } from "react-native"
import { Button } from "@/components/ui/button"
import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerTitle,
} from "@/components/ui/drawer"
import { Icon } from "@/components/ui/icon"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Screen } from "@/components/ui/screen"
import { Spinner } from "@/components/ui/spinner"
import { Text } from "@/components/ui/text"

const EMAIL_OPTIONS: {
  value: "share" | "hide"
  label: string
  hint: string
}[] = [
  { value: "share", label: "Share My Email", hint: "jamie@example.com" },
  {
    value: "hide",
    label: "Hide My Email",
    hint: "A unique address that forwards to you",
  },
]

function LoginSheet({
  onContinueWithEmail,
  onContinue,
}: {
  onContinueWithEmail?: () => void
  onContinue?: (email: "share" | "hide") => void
}) {
  const [open, setOpen] = React.useState(false)
  const [email, setEmail] = React.useState<"share" | "hide">("hide")
  const [busy, setBusy] = React.useState(false)
  const [done, setDone] = React.useState(false)

  React.useEffect(() => {
    if (!busy) return
    const timer = setTimeout(() => {
      setBusy(false)
      setOpen(false)
      setDone(true)
    }, 1000)
    return () => clearTimeout(timer)
  }, [busy])

  return (
    <Screen>
      <View className="flex-1 items-center px-7 pt-[5.625rem] pb-[2.125rem]">
        <View className="mb-5 size-[5.25rem] rounded-[1.25rem] bg-green-500" />
        <Text role="heading" className="text-center text-3xl font-semibold">
          {done ? "Welcome, Jamie" : "Trail Routes"}
        </Text>
        <Text className="mt-2 text-center text-base text-muted-foreground">
          {done
            ? "Your account was created with suiss."
            : "Save your routes and share them with friends."}
        </Text>
        <View className="flex-1" />
        {done ? (
          <Pressable
            role="link"
            hitSlop={8}
            onPress={() => setDone(false)}
            className="active:opacity-60"
          >
            <Text className="text-base text-primary">Sign out ›</Text>
          </Pressable>
        ) : (
          <View className="w-full gap-2.5">
            <Button
              size="xl"
              block
              onPress={() => setOpen(true)}
              className="h-[3.125rem] bg-foreground active:opacity-80"
            >
              <Text className="text-lg font-semibold text-background">
                Sign in with suiss
              </Text>
            </Button>
            <Button
              size="xl"
              block
              variant="secondary"
              onPress={onContinueWithEmail}
              className="h-[3.125rem]"
            >
              <Text className="text-lg font-semibold">Continue with email</Text>
            </Button>
          </View>
        )}
      </View>
      <Drawer open={open} onOpenChange={setOpen}>
        <DrawerContent className="gap-3.5 px-5 pt-3.5 pb-4">
          <View className="flex-row items-center">
            <View className="w-7" />
            <Text className="flex-1 text-center text-lg font-bold">suiss</Text>
            <DrawerClose
              render={
                <Button
                  variant="secondary"
                  size="icon-sm"
                  className="size-7"
                  accessibilityLabel="Close"
                />
              }
            >
              <Icon
                as={XIcon}
                weight="bold"
                className="size-3.5 text-muted-foreground"
              />
            </DrawerClose>
          </View>
          <DrawerTitle className="text-2xl font-semibold">
            Use your suiss Account with Trail Routes
          </DrawerTitle>
          <View className="rounded-xl bg-muted">
            <View className="flex-row items-center gap-3 px-3.5 py-2.5">
              <Icon as={UserIcon} className="size-5 text-muted-foreground" />
              <View>
                <Text className="text-xs text-muted-foreground">Name</Text>
                <Text className="text-base">Jamie Rivera</Text>
              </View>
            </View>
            <View className="ms-[2.875rem] h-px bg-border" />
            <RadioGroup
              value={email}
              onValueChange={setEmail}
              accessibilityLabel="Email"
              className="gap-0"
            >
              {EMAIL_OPTIONS.map((option, index) => (
                <Pressable
                  key={option.value}
                  onPress={() => setEmail(option.value)}
                  className={cn(
                    "flex-row items-center gap-3 py-2.5 pe-3.5",
                    index === 0
                      ? "ps-3.5"
                      : "ms-[2.875rem] border-t border-border"
                  )}
                >
                  {index === 0 ? (
                    <Icon
                      as={EnvelopeSimpleIcon}
                      className="size-5 text-muted-foreground"
                    />
                  ) : null}
                  <View className="flex-1">
                    <Text className="text-base">{option.label}</Text>
                    <Text className="text-xs text-muted-foreground">
                      {option.hint}
                    </Text>
                  </View>
                  <RadioGroupItem
                    value={option.value}
                    accessibilityLabel={option.label}
                    className="size-[1.375rem]"
                  />
                </Pressable>
              ))}
            </RadioGroup>
          </View>
          <Button
            size="xl"
            block
            onPress={() => {
              onContinue?.(email)
              setBusy(true)
            }}
            disabled={busy}
            className="h-[3.125rem]"
          >
            {busy ? (
              <Spinner size={20} colorClassName="text-primary-foreground" />
            ) : (
              <Text className="text-lg font-semibold">
                Continue with face scan
              </Text>
            )}
          </Button>
        </DrawerContent>
      </Drawer>
    </Screen>
  )
}

export { LoginSheet }
```

4. Update the import paths to match your project.

**StyleSheet**

1. Install the dependencies:

```bash title="Terminal"
npx expo install phosphor-react-native react-native-svg
```

2. Add the pieces it builds on: [Button](https://ui.suiss.app/docs/components/button), [Drawer](https://ui.suiss.app/docs/components/drawer), [Icon](https://ui.suiss.app/docs/components/icon), [Radio Group](https://ui.suiss.app/docs/components/radio-group), [Screen](https://ui.suiss.app/docs/components/screen), [Spinner](https://ui.suiss.app/docs/components/spinner), [Text](https://ui.suiss.app/docs/components/text).

3. Copy the source into `components/blocks/login-sheet.tsx`:

```tsx title="stylesheet/components/blocks/login-sheet.tsx"
import { EnvelopeSimpleIcon, UserIcon, XIcon } from "phosphor-react-native"
import * as React from "react"
import { Pressable, StyleSheet, View } from "react-native"
import { Button } from "@/components/ui/button"
import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerTitle,
} from "@/components/ui/drawer"
import { Icon } from "@/components/ui/icon"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Screen } from "@/components/ui/screen"
import { Spinner } from "@/components/ui/spinner"
import { Text } from "@/components/ui/text"
import { fontSize, palette, radius, useTheme } from "@/components/ui/theme"

const EMAIL_OPTIONS: {
  value: "share" | "hide"
  label: string
  hint: string
}[] = [
  { value: "share", label: "Share My Email", hint: "jamie@example.com" },
  {
    value: "hide",
    label: "Hide My Email",
    hint: "A unique address that forwards to you",
  },
]

function withAlpha(color: string, alpha: number) {
  if (!color.startsWith("#") || color.length !== 7) return color
  const r = Number.parseInt(color.slice(1, 3), 16)
  const g = Number.parseInt(color.slice(3, 5), 16)
  const b = Number.parseInt(color.slice(5, 7), 16)
  return `rgba(${r}, ${g}, ${b}, ${alpha})`
}

const styles = StyleSheet.create({
  body: {
    flex: 1,
    alignItems: "center",
    paddingHorizontal: 28,
    paddingTop: 90,
    paddingBottom: 34,
  },
  appIcon: {
    marginBottom: 20,
    width: 84,
    height: 84,
    borderRadius: 20,
  },
  title: { ...fontSize["3xl"], textAlign: "center", fontWeight: "600" },
  actions: { width: "100%", gap: 10 },
  button: { height: 50 },
  buttonText: { ...fontSize.lg, fontWeight: "600" },
  sheet: {
    gap: 14,
    paddingHorizontal: 20,
    paddingTop: 14,
    paddingBottom: 16,
  },
  sheetHeader: { flexDirection: "row", alignItems: "center" },
  brand: {
    ...fontSize.lg,
    flex: 1,
    textAlign: "center",
    fontWeight: "700",
  },
  close: { width: 28, height: 28 },
  card: { borderRadius: radius.xl },
  nameRow: {
    flexDirection: "row",
    alignItems: "center",
    gap: 12,
    paddingHorizontal: 14,
    paddingVertical: 10,
  },
  option: {
    flexDirection: "row",
    alignItems: "center",
    gap: 12,
    paddingVertical: 10,
    paddingEnd: 14,
  },
  radio: { width: 22, height: 22 },
})

function LoginSheet({
  onContinueWithEmail,
  onContinue,
}: {
  onContinueWithEmail?: () => void
  onContinue?: (email: "share" | "hide") => void
}) {
  const { colors } = useTheme()
  const [open, setOpen] = React.useState(false)
  const [email, setEmail] = React.useState<"share" | "hide">("hide")
  const [busy, setBusy] = React.useState(false)
  const [done, setDone] = React.useState(false)

  React.useEffect(() => {
    if (!busy) return
    const timer = setTimeout(() => {
      setBusy(false)
      setOpen(false)
      setDone(true)
    }, 1000)
    return () => clearTimeout(timer)
  }, [busy])

  const secondary = { color: colors.mutedForeground }

  return (
    <Screen>
      <View style={styles.body}>
        <View style={[styles.appIcon, { backgroundColor: palette.green500 }]} />
        <Text role="heading" style={styles.title}>
          {done ? "Welcome, Jamie" : "Trail Routes"}
        </Text>
        <Text
          style={[
            fontSize.base,
            { marginTop: 8, textAlign: "center" },
            secondary,
          ]}
        >
          {done
            ? "Your account was created with suiss."
            : "Save your routes and share them with friends."}
        </Text>
        <View style={{ flex: 1 }} />
        {done ? (
          <Pressable
            role="link"
            hitSlop={8}
            onPress={() => setDone(false)}
            style={({ pressed }) => pressed && { opacity: 0.6 }}
          >
            <Text style={[fontSize.base, { color: colors.primary }]}>
              Sign out ›
            </Text>
          </Pressable>
        ) : (
          <View style={styles.actions}>
            <Button
              size="xl"
              block
              onPress={() => setOpen(true)}
              style={({ pressed }) => [
                styles.button,
                {
                  backgroundColor: pressed
                    ? withAlpha(colors.foreground, 0.8)
                    : colors.foreground,
                },
              ]}
            >
              <Text style={[styles.buttonText, { color: colors.background }]}>
                Sign in with suiss
              </Text>
            </Button>
            <Button
              size="xl"
              block
              variant="secondary"
              onPress={onContinueWithEmail}
              style={styles.button}
            >
              <Text style={styles.buttonText}>Continue with email</Text>
            </Button>
          </View>
        )}
      </View>
      <Drawer open={open} onOpenChange={setOpen}>
        <DrawerContent style={styles.sheet}>
          <View style={styles.sheetHeader}>
            <View style={{ width: 28 }} />
            <Text style={styles.brand}>suiss</Text>
            <DrawerClose
              render={
                <Button
                  variant="secondary"
                  size="icon-sm"
                  style={styles.close}
                  accessibilityLabel="Close"
                />
              }
            >
              <Icon
                as={XIcon}
                weight="bold"
                size={14}
                color={colors.mutedForeground}
              />
            </DrawerClose>
          </View>
          <DrawerTitle style={{ ...fontSize["2xl"], fontWeight: "600" }}>
            Use your suiss Account with Trail Routes
          </DrawerTitle>
          <View style={[styles.card, { backgroundColor: colors.muted }]}>
            <View style={styles.nameRow}>
              <Icon as={UserIcon} size={20} color={colors.mutedForeground} />
              <View>
                <Text style={[fontSize.xs, secondary]}>Name</Text>
                <Text style={fontSize.base}>Jamie Rivera</Text>
              </View>
            </View>
            <View
              style={{
                marginStart: 46,
                height: 1,
                backgroundColor: colors.border,
              }}
            />
            <RadioGroup
              value={email}
              onValueChange={setEmail}
              accessibilityLabel="Email"
              style={{ gap: 0 }}
            >
              {EMAIL_OPTIONS.map((option, index) => (
                <Pressable
                  key={option.value}
                  onPress={() => setEmail(option.value)}
                  style={[
                    styles.option,
                    index === 0
                      ? { paddingStart: 14 }
                      : {
                          marginStart: 46,
                          borderTopWidth: 1,
                          borderTopColor: colors.border,
                        },
                  ]}
                >
                  {index === 0 ? (
                    <Icon
                      as={EnvelopeSimpleIcon}
                      size={20}
                      color={colors.mutedForeground}
                    />
                  ) : null}
                  <View style={{ flex: 1 }}>
                    <Text style={fontSize.base}>{option.label}</Text>
                    <Text style={[fontSize.xs, secondary]}>{option.hint}</Text>
                  </View>
                  <RadioGroupItem
                    value={option.value}
                    accessibilityLabel={option.label}
                    style={styles.radio}
                  />
                </Pressable>
              ))}
            </RadioGroup>
          </View>
          <Button
            size="xl"
            block
            onPress={() => {
              onContinue?.(email)
              setBusy(true)
            }}
            disabled={busy}
            style={styles.button}
          >
            {busy ? (
              <Spinner size={20} color={colors.primaryForeground} />
            ) : (
              <Text style={styles.buttonText}>Continue with face scan</Text>
            )}
          </Button>
        </DrawerContent>
      </Drawer>
    </Screen>
  )
}

export { LoginSheet }
```

4. Update the import paths to match your project.

**SwiftUI**

1. Target iOS 17 or later.

2. Add the Swift package to your app:

```swift title="Package.swift"
.package(url: "https://github.com/phosphor-icons/swift", from: "2.1.0")
```

3. Copy the theme files every view uses:

- `swift/Theme/Theme.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Theme/Theme.swift)

- `swift/Theme/Tokens.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Theme/Tokens.swift)

4. Copy the source and every file it uses:

- `swift/Blocks/LoginSheet.swift`: shown above

- `swift/Components/BottomBar.swift`: see [Bottom Bar](https://ui.suiss.app/docs/components/bottom-bar.md)

- `swift/Components/Button.swift`: see [Button](https://ui.suiss.app/docs/components/button.md)

- `swift/Components/ButtonGroup.swift`: see [Button Group](https://ui.suiss.app/docs/components/button-group.md)

- `swift/Components/Checkbox.swift`: see [Checkbox](https://ui.suiss.app/docs/components/checkbox.md)

- `swift/Components/Dialog.swift`: see [Dialog](https://ui.suiss.app/docs/components/dialog.md)

- `swift/Components/Drawer.swift`: see [Drawer](https://ui.suiss.app/docs/components/drawer.md)

- `swift/Components/DropdownMenu.swift`: see [Dropdown Menu](https://ui.suiss.app/docs/components/dropdown-menu.md)

- `swift/Components/Field.swift`: see [Field](https://ui.suiss.app/docs/components/field.md)

- `swift/Components/Haptics.swift`: see [Haptics](https://ui.suiss.app/docs/components/haptics.md)

- `swift/Components/KeyboardScrollView.swift`: see [Keyboard Scroll View](https://ui.suiss.app/docs/components/keyboard-scroll-view.md)

- `swift/Components/Label.swift`: see [Label](https://ui.suiss.app/docs/components/label.md)

- `swift/Components/Overlay.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Components/Overlay.swift)

- `swift/Components/RadioGroup.swift`: see [Radio Group](https://ui.suiss.app/docs/components/radio-group.md)

- `swift/Components/Screen.swift`: see [Screen](https://ui.suiss.app/docs/components/screen.md)

- `swift/Components/Select.swift`: see [Select](https://ui.suiss.app/docs/components/select.md)

- `swift/Components/Separator.swift`: see [Separator](https://ui.suiss.app/docs/components/separator.md)

- `swift/Components/Spinner.swift`: see [Spinner](https://ui.suiss.app/docs/components/spinner.md)

- `swift/Components/TextVariant.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Components/TextVariant.swift)

5. Add the files to your app target.

**Jetpack Compose**

1. Set `minSdk` to 24 or higher and add the dependencies:

```kotlin title="build.gradle.kts"
implementation(platform("androidx.compose:compose-bom:2026.09.00"))
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.ui:ui")
implementation("com.adamglin:phosphor-icon-android:1.0.0")
```

2. Copy the theme files every view uses:

- `kotlin/theme/Theme.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/theme/Theme.kt)

- `kotlin/theme/Tokens.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/theme/Tokens.kt)

3. Copy the source and every file it uses:

- `kotlin/blocks/LoginSheet.kt`: shown above

- `kotlin/components/BottomBar.kt`: see [Bottom Bar](https://ui.suiss.app/docs/components/bottom-bar.md)

- `kotlin/components/Button.kt`: see [Button](https://ui.suiss.app/docs/components/button.md)

- `kotlin/components/ButtonGroup.kt`: see [Button Group](https://ui.suiss.app/docs/components/button-group.md)

- `kotlin/components/Checkbox.kt`: see [Checkbox](https://ui.suiss.app/docs/components/checkbox.md)

- `kotlin/components/Content.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/components/Content.kt)

- `kotlin/components/Drawer.kt`: see [Drawer](https://ui.suiss.app/docs/components/drawer.md)

- `kotlin/components/Field.kt`: see [Field](https://ui.suiss.app/docs/components/field.md)

- `kotlin/components/FormSupport.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/components/FormSupport.kt)

- `kotlin/components/Haptics.kt`: see [Haptics](https://ui.suiss.app/docs/components/haptics.md)

- `kotlin/components/Icon.kt`: see [Icon](https://ui.suiss.app/docs/components/icon.md)

- `kotlin/components/Label.kt`: see [Label](https://ui.suiss.app/docs/components/label.md)

- `kotlin/components/Overlay.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/components/Overlay.kt)

- `kotlin/components/RadioGroup.kt`: see [Radio Group](https://ui.suiss.app/docs/components/radio-group.md)

- `kotlin/components/Screen.kt`: see [Screen](https://ui.suiss.app/docs/components/screen.md)

- `kotlin/components/Separator.kt`: see [Separator](https://ui.suiss.app/docs/components/separator.md)

- `kotlin/components/Spinner.kt`: see [Spinner](https://ui.suiss.app/docs/components/spinner.md)

4. Update the `com.esuiss.ui` package names to match your project.

## Usage

```tsx
import { LoginSheet } from "@/components/blocks/login-sheet"
```

```tsx
<LoginSheet />
```

Edit `EMAIL_OPTIONS` to change the email choices. Wire `onContinue` (receives `share` or `hide`) to your sign-in flow and `onContinueWithEmail` to your email sign-up screen.

