# Settings Account

An account settings page with a profile sidebar and a Sign-In and Security section of editable cards.

Settings Account 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/settings-account

**React**

**Example**

```tsx title="components/blocks/settings-account.tsx"
"use client"

import {
  CheckIcon,
  CloudIcon,
  CreditCardIcon,
  DeviceMobileIcon,
  KeyIcon,
  ShieldIcon,
  UserIcon,
  UsersIcon,
} from "@phosphor-icons/react"
import * as React from "react"

import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInset,
  SidebarMenu,
  SidebarMenuBadge,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarProvider,
  SidebarRail,
  SidebarTrigger,
  useSidebar,
} from "@/components/ui/sidebar"
import { Switch } from "@/components/ui/switch"

type NavItem = {
  value: string
  label: string
  icon: React.ReactNode
  badge?: React.ReactNode
  group?: string
}

function NavSidebar({
  items,
  value: valueProp,
  defaultValue,
  onValueChange,
  header,
  footer,
  collapsible = "icon",
}: {
  collapsible?: "offcanvas" | "icon"
  items: NavItem[]
  value?: string
  defaultValue?: string
  onValueChange?: (value: string) => void
  header?: React.ReactNode
  footer?: React.ReactNode
}) {
  const { isMobile, setOpenMobile } = useSidebar()
  const [uncontrolledValue, setUncontrolledValue] = React.useState(
    defaultValue ?? items[0]?.value
  )
  const value = valueProp ?? uncontrolledValue
  const groups = [...new Set(items.map((item) => item.group ?? ""))]

  return (
    <Sidebar collapsible={collapsible}>
      {header && (
        <SidebarHeader className="group-data-[collapsible=icon]:hidden">
          {header}
        </SidebarHeader>
      )}
      <SidebarContent>
        {groups.map((group) => (
          <SidebarGroup key={group}>
            {group && <SidebarGroupLabel>{group}</SidebarGroupLabel>}
            <SidebarGroupContent>
              <SidebarMenu>
                {items
                  .filter((item) => (item.group ?? "") === group)
                  .map((item) => (
                    <SidebarMenuItem key={item.value}>
                      <SidebarMenuButton
                        isActive={item.value === value}
                        tooltip={item.label}
                        aria-current={item.value === value ? "page" : undefined}
                        onClick={() => {
                          if (valueProp === undefined) {
                            setUncontrolledValue(item.value)
                          }
                          onValueChange?.(item.value)
                          if (isMobile) setOpenMobile(false)
                        }}
                      >
                        {item.icon}
                        <span>{item.label}</span>
                      </SidebarMenuButton>
                      {item.badge !== undefined && (
                        <SidebarMenuBadge>{item.badge}</SidebarMenuBadge>
                      )}
                    </SidebarMenuItem>
                  ))}
              </SidebarMenu>
            </SidebarGroupContent>
          </SidebarGroup>
        ))}
      </SidebarContent>
      {footer && (
        <SidebarFooter className="group-data-[collapsible=icon]:hidden">
          {footer}
        </SidebarFooter>
      )}
      <SidebarRail />
    </Sidebar>
  )
}

const items: NavItem[] = [
  {
    value: "personal",
    label: "Personal Information",
    icon: (
      <UserIcon />
    ),
  },
  {
    value: "security",
    label: "Sign-In & Security",
    icon: (
      <KeyIcon />
    ),
  },
  {
    value: "payment",
    label: "Payment & Shipping",
    icon: (
      <CreditCardIcon />
    ),
  },
  {
    value: "subscriptions",
    label: "Subscriptions",
    icon: (
      <CloudIcon />
    ),
  },
  {
    value: "family",
    label: "Family Sharing",
    icon: (
      <UsersIcon />
    ),
  },
  {
    value: "devices",
    label: "Devices",
    icon: (
      <DeviceMobileIcon />
    ),
  },
  {
    value: "privacy",
    label: "Privacy",
    icon: (
      <ShieldIcon />
    ),
  },
]

type Editable = "email" | "phone"

function EditableCard({
  title,
  field,
  value,
  editing,
  onEdit,
  onCancel,
  onSave,
}: {
  title: string
  field: Editable
  value: string
  editing: boolean
  onEdit: () => void
  onCancel: () => void
  onSave: (value: string) => void
}) {
  const [draft, setDraft] = React.useState(value)
  const inputRef = React.useRef<HTMLInputElement>(null)

  React.useEffect(() => {
    if (!editing) return
    setDraft(value)
    const timer = window.setTimeout(() => inputRef.current?.focus(), 300)
    return () => window.clearTimeout(timer)
  }, [editing, value])

  return (
    <div className="rounded-[1.125rem] bg-muted px-5 py-4.5">
      <div className="flex items-center gap-3">
        <div className="flex flex-1 flex-col gap-0.75">
          <h2 className="text-lg font-semibold">{title}</h2>
          {!editing && (
            <p className="text-base text-muted-foreground">{value}</p>
          )}
        </div>
        {!editing && (
          <button
            type="button"
            onClick={onEdit}
            aria-label={`Edit ${title.toLowerCase()}`}
            className="rounded-xs text-base text-primary outline-none hover:underline focus-visible:ring-3 focus-visible:ring-ring/50"
          >
            Edit ›
          </button>
        )}
      </div>
      <div
        data-open={editing ? "" : undefined}
        inert={!editing}
        className="grid grid-rows-[0fr] transition-[grid-template-rows] duration-400 ease-[cubic-bezier(0.32,0.72,0,1)] data-open:grid-rows-[1fr] motion-reduce:transition-none"
      >
        <div className="min-h-0 overflow-hidden">
          <form
            onSubmit={(event) => {
              event.preventDefault()
              onSave(draft)
            }}
            className="flex flex-col gap-3 pt-3.5"
          >
            <Input
              ref={inputRef}
              name={field}
              value={draft}
              onChange={(event) => setDraft(event.target.value)}
              aria-label={title}
              className="h-12 rounded-xl border-input bg-background text-lg"
            />
            <div className="flex gap-2.5">
              <Button type="submit" size="sm">
                Save
              </Button>
              <Button
                type="button"
                size="sm"
                variant="outline"
                onClick={onCancel}
              >
                Cancel
              </Button>
            </div>
          </form>
        </div>
      </div>
    </div>
  )
}

function Security() {
  const idPrefix = React.useId()
  const [values, setValues] = React.useState({
    email: "jamie@example.com",
    phone: "+1 (415) 555-0142",
  })
  const [editing, setEditing] = React.useState<Editable | null>(null)
  const [passkeys, setPasskeys] = React.useState(true)
  const [saved, setSaved] = React.useState<string | null>(null)

  React.useEffect(() => {
    if (!saved) return
    const timer = window.setTimeout(() => setSaved(null), 2200)
    return () => window.clearTimeout(timer)
  }, [saved])

  const cards: { field: Editable; title: string }[] = [
    { field: "email", title: "Email & phone" },
    { field: "phone", title: "Trusted phone number" },
  ]

  return (
    <div className="flex flex-col gap-3.5">
      {cards.map((card) => (
        <EditableCard
          key={card.field}
          title={card.title}
          field={card.field}
          value={values[card.field]}
          editing={editing === card.field}
          onEdit={() => setEditing(card.field)}
          onCancel={() => setEditing(null)}
          onSave={(value) => {
            setValues((current) => ({ ...current, [card.field]: value }))
            setEditing(null)
            setSaved(card.title)
          }}
        />
      ))}
      <div className="flex items-center gap-3 rounded-[1.125rem] bg-muted px-5 py-4.5">
        <div className="flex flex-1 flex-col gap-0.75">
          <label
            htmlFor={`${idPrefix}-passkeys`}
            className="text-lg font-semibold"
          >
            Passkeys
          </label>
          <p className="text-base text-muted-foreground">
            {passkeys ? "On for 3 devices" : "Off"}
          </p>
        </div>
        <Switch
          id={`${idPrefix}-passkeys`}
          checked={passkeys}
          onCheckedChange={setPasskeys}
        />
      </div>
      <div className="flex items-center gap-3 rounded-[1.125rem] bg-muted px-5 py-4.5">
        <div className="flex flex-1 flex-col gap-0.75">
          <h2 className="text-lg font-semibold">Two-factor authentication</h2>
          <p className="text-base text-muted-foreground">
            Codes are sent to your trusted devices.
          </p>
        </div>
        <span className="text-sm font-semibold text-[color-mix(in_oklab,var(--color-green-500),var(--foreground)_45%)] dark:text-green-500">
          On
        </span>
      </div>
      <p role="status" className="flex min-h-6 items-center gap-1.5 text-sm">
        {saved && (
          <span className="flex items-center gap-1.5 transition-[opacity,translate] duration-800 starting:translate-y-1.5 starting:opacity-0 motion-reduce:transition-none">
            <CheckIcon
              className="size-4 text-[color-mix(in_oklab,var(--color-green-500),var(--foreground)_30%)] dark:text-green-500"
            />
            {saved} saved.
          </span>
        )}
      </p>
    </div>
  )
}

export function SettingsAccount() {
  const [section, setSection] = React.useState("security")
  const current = items.find((item) => item.value === section) ?? items[1]

  return (
    <SidebarProvider>
      <NavSidebar
        items={items}
        value={section}
        onValueChange={setSection}
        collapsible="offcanvas"
        header={
          <div className="flex flex-col gap-2 px-2 pt-2 pb-1">
            <Avatar className="size-18">
              <AvatarFallback className="bg-linear-to-br from-[oklch(0.78_0.1_250)] to-[oklch(0.6_0.15_280)] text-2xl text-white dark:from-[oklch(0.78_0.1_250)] dark:to-[oklch(0.6_0.15_280)]">
                JR
              </AvatarFallback>
            </Avatar>
            <p className="text-2xl font-semibold tracking-tight">
              Jamie Rivera
            </p>
            <p className="text-sm text-muted-foreground">jamie@example.com</p>
          </div>
        }
      />
      <SidebarInset>
        <div className="mx-auto flex w-full max-w-170 flex-col gap-3.5 px-5 py-8 md:px-9">
          <div className="flex items-center gap-2">
            <SidebarTrigger className="-ms-2" />
            <h1 className="text-4xl font-semibold tracking-tight">
              {current?.label}
            </h1>
          </div>
          <div
            key={section}
            className="transition-[opacity,translate] duration-1000 ease-[cubic-bezier(0.32,0.72,0,1)] starting:translate-y-2 starting:opacity-0 motion-reduce:transition-none"
          >
            {section === "security" ? (
              <Security />
            ) : (
              <p className="text-lg text-muted-foreground">
                This section isn't part of the sample. Choose Sign-In &
                Security.
              </p>
            )}
          </div>
        </div>
      </SidebarInset>
    </SidebarProvider>
  )
}
```

An account settings page with a profile header and a sidebar of sections. Only Sign-In and Security is filled in: email and phone cards that expand into an inline edit form, a Passkeys switch, a two-factor row and a confirmation after each save, while the other sections show a placeholder. It uses a `Sidebar` that slides off canvas. Use it as the frame for any account or profile settings.

## Installation

### CLI

```bash title="Terminal"
npx shadcn@latest add @esuiss/settings-account
```

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 settings-account
```

### Manual

1. Install the dependencies:

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

2. Add the pieces it builds on: [Avatar](https://ui.suiss.app/docs/components/avatar), [Button](https://ui.suiss.app/docs/components/button), [Input](https://ui.suiss.app/docs/components/input), [Sidebar](https://ui.suiss.app/docs/components/sidebar), [Switch](https://ui.suiss.app/docs/components/switch).

3. Copy the source into `components/blocks/settings-account.tsx`:

```tsx title="components/blocks/settings-account.tsx"
"use client"

import {
  CheckIcon,
  CloudIcon,
  CreditCardIcon,
  DeviceMobileIcon,
  KeyIcon,
  ShieldIcon,
  UserIcon,
  UsersIcon,
} from "@phosphor-icons/react"
import * as React from "react"

import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInset,
  SidebarMenu,
  SidebarMenuBadge,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarProvider,
  SidebarRail,
  SidebarTrigger,
  useSidebar,
} from "@/components/ui/sidebar"
import { Switch } from "@/components/ui/switch"

type NavItem = {
  value: string
  label: string
  icon: React.ReactNode
  badge?: React.ReactNode
  group?: string
}

function NavSidebar({
  items,
  value: valueProp,
  defaultValue,
  onValueChange,
  header,
  footer,
  collapsible = "icon",
}: {
  collapsible?: "offcanvas" | "icon"
  items: NavItem[]
  value?: string
  defaultValue?: string
  onValueChange?: (value: string) => void
  header?: React.ReactNode
  footer?: React.ReactNode
}) {
  const { isMobile, setOpenMobile } = useSidebar()
  const [uncontrolledValue, setUncontrolledValue] = React.useState(
    defaultValue ?? items[0]?.value
  )
  const value = valueProp ?? uncontrolledValue
  const groups = [...new Set(items.map((item) => item.group ?? ""))]

  return (
    <Sidebar collapsible={collapsible}>
      {header && (
        <SidebarHeader className="group-data-[collapsible=icon]:hidden">
          {header}
        </SidebarHeader>
      )}
      <SidebarContent>
        {groups.map((group) => (
          <SidebarGroup key={group}>
            {group && <SidebarGroupLabel>{group}</SidebarGroupLabel>}
            <SidebarGroupContent>
              <SidebarMenu>
                {items
                  .filter((item) => (item.group ?? "") === group)
                  .map((item) => (
                    <SidebarMenuItem key={item.value}>
                      <SidebarMenuButton
                        isActive={item.value === value}
                        tooltip={item.label}
                        aria-current={item.value === value ? "page" : undefined}
                        onClick={() => {
                          if (valueProp === undefined) {
                            setUncontrolledValue(item.value)
                          }
                          onValueChange?.(item.value)
                          if (isMobile) setOpenMobile(false)
                        }}
                      >
                        {item.icon}
                        <span>{item.label}</span>
                      </SidebarMenuButton>
                      {item.badge !== undefined && (
                        <SidebarMenuBadge>{item.badge}</SidebarMenuBadge>
                      )}
                    </SidebarMenuItem>
                  ))}
              </SidebarMenu>
            </SidebarGroupContent>
          </SidebarGroup>
        ))}
      </SidebarContent>
      {footer && (
        <SidebarFooter className="group-data-[collapsible=icon]:hidden">
          {footer}
        </SidebarFooter>
      )}
      <SidebarRail />
    </Sidebar>
  )
}

const items: NavItem[] = [
  {
    value: "personal",
    label: "Personal Information",
    icon: (
      <UserIcon />
    ),
  },
  {
    value: "security",
    label: "Sign-In & Security",
    icon: (
      <KeyIcon />
    ),
  },
  {
    value: "payment",
    label: "Payment & Shipping",
    icon: (
      <CreditCardIcon />
    ),
  },
  {
    value: "subscriptions",
    label: "Subscriptions",
    icon: (
      <CloudIcon />
    ),
  },
  {
    value: "family",
    label: "Family Sharing",
    icon: (
      <UsersIcon />
    ),
  },
  {
    value: "devices",
    label: "Devices",
    icon: (
      <DeviceMobileIcon />
    ),
  },
  {
    value: "privacy",
    label: "Privacy",
    icon: (
      <ShieldIcon />
    ),
  },
]

type Editable = "email" | "phone"

function EditableCard({
  title,
  field,
  value,
  editing,
  onEdit,
  onCancel,
  onSave,
}: {
  title: string
  field: Editable
  value: string
  editing: boolean
  onEdit: () => void
  onCancel: () => void
  onSave: (value: string) => void
}) {
  const [draft, setDraft] = React.useState(value)
  const inputRef = React.useRef<HTMLInputElement>(null)

  React.useEffect(() => {
    if (!editing) return
    setDraft(value)
    const timer = window.setTimeout(() => inputRef.current?.focus(), 300)
    return () => window.clearTimeout(timer)
  }, [editing, value])

  return (
    <div className="rounded-[1.125rem] bg-muted px-5 py-4.5">
      <div className="flex items-center gap-3">
        <div className="flex flex-1 flex-col gap-0.75">
          <h2 className="text-lg font-semibold">{title}</h2>
          {!editing && (
            <p className="text-base text-muted-foreground">{value}</p>
          )}
        </div>
        {!editing && (
          <button
            type="button"
            onClick={onEdit}
            aria-label={`Edit ${title.toLowerCase()}`}
            className="rounded-xs text-base text-primary outline-none hover:underline focus-visible:ring-3 focus-visible:ring-ring/50"
          >
            Edit ›
          </button>
        )}
      </div>
      <div
        data-open={editing ? "" : undefined}
        inert={!editing}
        className="grid grid-rows-[0fr] transition-[grid-template-rows] duration-400 ease-[cubic-bezier(0.32,0.72,0,1)] data-open:grid-rows-[1fr] motion-reduce:transition-none"
      >
        <div className="min-h-0 overflow-hidden">
          <form
            onSubmit={(event) => {
              event.preventDefault()
              onSave(draft)
            }}
            className="flex flex-col gap-3 pt-3.5"
          >
            <Input
              ref={inputRef}
              name={field}
              value={draft}
              onChange={(event) => setDraft(event.target.value)}
              aria-label={title}
              className="h-12 rounded-xl border-input bg-background text-lg"
            />
            <div className="flex gap-2.5">
              <Button type="submit" size="sm">
                Save
              </Button>
              <Button
                type="button"
                size="sm"
                variant="outline"
                onClick={onCancel}
              >
                Cancel
              </Button>
            </div>
          </form>
        </div>
      </div>
    </div>
  )
}

function Security() {
  const idPrefix = React.useId()
  const [values, setValues] = React.useState({
    email: "jamie@example.com",
    phone: "+1 (415) 555-0142",
  })
  const [editing, setEditing] = React.useState<Editable | null>(null)
  const [passkeys, setPasskeys] = React.useState(true)
  const [saved, setSaved] = React.useState<string | null>(null)

  React.useEffect(() => {
    if (!saved) return
    const timer = window.setTimeout(() => setSaved(null), 2200)
    return () => window.clearTimeout(timer)
  }, [saved])

  const cards: { field: Editable; title: string }[] = [
    { field: "email", title: "Email & phone" },
    { field: "phone", title: "Trusted phone number" },
  ]

  return (
    <div className="flex flex-col gap-3.5">
      {cards.map((card) => (
        <EditableCard
          key={card.field}
          title={card.title}
          field={card.field}
          value={values[card.field]}
          editing={editing === card.field}
          onEdit={() => setEditing(card.field)}
          onCancel={() => setEditing(null)}
          onSave={(value) => {
            setValues((current) => ({ ...current, [card.field]: value }))
            setEditing(null)
            setSaved(card.title)
          }}
        />
      ))}
      <div className="flex items-center gap-3 rounded-[1.125rem] bg-muted px-5 py-4.5">
        <div className="flex flex-1 flex-col gap-0.75">
          <label
            htmlFor={`${idPrefix}-passkeys`}
            className="text-lg font-semibold"
          >
            Passkeys
          </label>
          <p className="text-base text-muted-foreground">
            {passkeys ? "On for 3 devices" : "Off"}
          </p>
        </div>
        <Switch
          id={`${idPrefix}-passkeys`}
          checked={passkeys}
          onCheckedChange={setPasskeys}
        />
      </div>
      <div className="flex items-center gap-3 rounded-[1.125rem] bg-muted px-5 py-4.5">
        <div className="flex flex-1 flex-col gap-0.75">
          <h2 className="text-lg font-semibold">Two-factor authentication</h2>
          <p className="text-base text-muted-foreground">
            Codes are sent to your trusted devices.
          </p>
        </div>
        <span className="text-sm font-semibold text-[color-mix(in_oklab,var(--color-green-500),var(--foreground)_45%)] dark:text-green-500">
          On
        </span>
      </div>
      <p role="status" className="flex min-h-6 items-center gap-1.5 text-sm">
        {saved && (
          <span className="flex items-center gap-1.5 transition-[opacity,translate] duration-800 starting:translate-y-1.5 starting:opacity-0 motion-reduce:transition-none">
            <CheckIcon
              className="size-4 text-[color-mix(in_oklab,var(--color-green-500),var(--foreground)_30%)] dark:text-green-500"
            />
            {saved} saved.
          </span>
        )}
      </p>
    </div>
  )
}

export function SettingsAccount() {
  const [section, setSection] = React.useState("security")
  const current = items.find((item) => item.value === section) ?? items[1]

  return (
    <SidebarProvider>
      <NavSidebar
        items={items}
        value={section}
        onValueChange={setSection}
        collapsible="offcanvas"
        header={
          <div className="flex flex-col gap-2 px-2 pt-2 pb-1">
            <Avatar className="size-18">
              <AvatarFallback className="bg-linear-to-br from-[oklch(0.78_0.1_250)] to-[oklch(0.6_0.15_280)] text-2xl text-white dark:from-[oklch(0.78_0.1_250)] dark:to-[oklch(0.6_0.15_280)]">
                JR
              </AvatarFallback>
            </Avatar>
            <p className="text-2xl font-semibold tracking-tight">
              Jamie Rivera
            </p>
            <p className="text-sm text-muted-foreground">jamie@example.com</p>
          </div>
        }
      />
      <SidebarInset>
        <div className="mx-auto flex w-full max-w-170 flex-col gap-3.5 px-5 py-8 md:px-9">
          <div className="flex items-center gap-2">
            <SidebarTrigger className="-ms-2" />
            <h1 className="text-4xl font-semibold tracking-tight">
              {current?.label}
            </h1>
          </div>
          <div
            key={section}
            className="transition-[opacity,translate] duration-1000 ease-[cubic-bezier(0.32,0.72,0,1)] starting:translate-y-2 starting:opacity-0 motion-reduce:transition-none"
          >
            {section === "security" ? (
              <Security />
            ) : (
              <p className="text-lg text-muted-foreground">
                This section isn't part of the sample. Choose Sign-In &
                Security.
              </p>
            )}
          </div>
        </div>
      </SidebarInset>
    </SidebarProvider>
  )
}
```

4. Update the import paths to match your project.

## Usage

```tsx
import { SettingsAccount } from "@/components/blocks/settings-account"
```

```tsx
<SettingsAccount />
```

Load the email, phone and profile details from the signed-in user, and replace the `onSave` handler with your account API. Add content for the remaining sections listed in `items`. The profile name, email and initials in the header are hard-coded.

## Notes

Collapsed edit forms are `inert`, focus moves to the input when a card opens, and save confirmations are announced.

**React Native, SwiftUI, Jetpack Compose**

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

```tsx title="uniwind/components/blocks/settings-account.tsx"
import { cn } from "cn"
import {
  CaretRightIcon,
  CheckIcon,
  CloudIcon,
  CreditCardIcon,
  DeviceMobileIcon,
  KeyIcon,
  type Icon as PhosphorIcon,
  ShieldIcon,
  UserIcon,
  UsersIcon,
} from "phosphor-react-native"
import * as React from "react"
import { I18nManager, Pressable, View } from "react-native"
import { FadeIn, FadeInDown, ReduceMotion } from "react-native-reanimated"
import { AnimatedView } from "@/components/ui/animated-view"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
  Header,
  HeaderBack,
  HeaderLargeTitle,
  HeaderScrollView,
  HeaderTitle,
  useHeaderScroll,
} from "@/components/ui/header"
import { Icon } from "@/components/ui/icon"
import { Input } from "@/components/ui/input"
import {
  Item,
  ItemActions,
  ItemContent,
  ItemGroup,
  ItemMedia,
  ItemSeparator,
  ItemTitle,
} from "@/components/ui/item"
import { Screen } from "@/components/ui/screen"
import { Switch } from "@/components/ui/switch"
import { Text } from "@/components/ui/text"

const ITEMS: { value: string; label: string; icon: PhosphorIcon }[] = [
  { value: "personal", label: "Personal Information", icon: UserIcon },
  { value: "security", label: "Sign-In & Security", icon: KeyIcon },
  { value: "payment", label: "Payment & Shipping", icon: CreditCardIcon },
  { value: "subscriptions", label: "Subscriptions", icon: CloudIcon },
  { value: "family", label: "Family Sharing", icon: UsersIcon },
  { value: "devices", label: "Devices", icon: DeviceMobileIcon },
  { value: "privacy", label: "Privacy", icon: ShieldIcon },
]

const CARD_CLASS = "rounded-[1.125rem] bg-muted px-5 py-4.5"

type Editable = "email" | "phone"

function EditableCard({
  title,
  value,
  editing,
  onEdit,
  onCancel,
  onSave,
}: {
  title: string
  value: string
  editing: boolean
  onEdit: () => void
  onCancel: () => void
  onSave: (value: string) => void
}) {
  const [draft, setDraft] = React.useState(value)

  React.useEffect(() => {
    if (editing) setDraft(value)
  }, [editing, value])

  return (
    <View className={CARD_CLASS}>
      <View className="flex-row items-center gap-3">
        <View className="flex-1 gap-0.75">
          <Text role="heading" className="text-lg font-semibold">
            {title}
          </Text>
          {editing ? null : (
            <Text className="text-base text-muted-foreground">{value}</Text>
          )}
        </View>
        {editing ? null : (
          <Pressable
            role="button"
            hitSlop={8}
            onPress={onEdit}
            accessibilityLabel={`Edit ${title.toLowerCase()}`}
            className="active:opacity-60"
          >
            <Text className="text-base text-primary">Edit ›</Text>
          </Pressable>
        )}
      </View>
      {editing ? (
        <AnimatedView
          entering={FadeIn.duration(300).reduceMotion(ReduceMotion.System)}
          className="gap-3 pt-3.5"
        >
          <Input
            autoFocus
            value={draft}
            onChangeText={setDraft}
            accessibilityLabel={title}
            returnKeyType="done"
            onSubmitEditing={() => onSave(draft)}
            className={(state) =>
              cn(
                "h-12 rounded-xl bg-background text-[18px]",
                state.focused
                  ? "border-ring ring-3 ring-ring/50"
                  : "border-input"
              )
            }
          />
          <View className="flex-row gap-2.5">
            <Button size="sm" onPress={() => onSave(draft)}>
              <Text>Save</Text>
            </Button>
            <Button size="sm" variant="outline" onPress={onCancel}>
              <Text>Cancel</Text>
            </Button>
          </View>
        </AnimatedView>
      ) : null}
    </View>
  )
}

function Security({
  onSave,
}: {
  onSave?: (field: Editable, value: string) => void
}) {
  const [values, setValues] = React.useState<Record<Editable, string>>({
    email: "jamie@example.com",
    phone: "+1 (415) 555-0142",
  })
  const [editing, setEditing] = React.useState<Editable | null>(null)
  const [passkeys, setPasskeys] = React.useState(true)
  const [saved, setSaved] = React.useState<string | null>(null)

  React.useEffect(() => {
    if (!saved) return
    const timer = setTimeout(() => setSaved(null), 2200)
    return () => clearTimeout(timer)
  }, [saved])

  const cards: { field: Editable; title: string }[] = [
    { field: "email", title: "Email & phone" },
    { field: "phone", title: "Trusted phone number" },
  ]

  return (
    <View className="gap-3.5">
      {cards.map((card) => (
        <EditableCard
          key={card.field}
          title={card.title}
          value={values[card.field]}
          editing={editing === card.field}
          onEdit={() => setEditing(card.field)}
          onCancel={() => setEditing(null)}
          onSave={(value) => {
            onSave?.(card.field, value)
            setValues((current) => ({ ...current, [card.field]: value }))
            setEditing(null)
            setSaved(card.title)
          }}
        />
      ))}
      <View className={cn(CARD_CLASS, "flex-row items-center gap-3")}>
        <View className="flex-1 gap-0.75">
          <Text
            nativeID="settings-account-passkeys"
            className="text-lg font-semibold"
          >
            Passkeys
          </Text>
          <Text className="text-base text-muted-foreground">
            {passkeys ? "On for 3 devices" : "Off"}
          </Text>
        </View>
        <Switch
          checked={passkeys}
          onCheckedChange={setPasskeys}
          accessibilityLabelledBy="settings-account-passkeys"
        />
      </View>
      <View className={cn(CARD_CLASS, "flex-row items-center gap-3")}>
        <View className="flex-1 gap-0.75">
          <Text role="heading" className="text-lg font-semibold">
            Two-factor authentication
          </Text>
          <Text className="text-base text-muted-foreground">
            Codes are sent to your trusted devices.
          </Text>
        </View>
        <Text className="text-sm font-semibold text-green-600 dark:text-green-400">
          On
        </Text>
      </View>
      <View
        accessibilityLiveRegion="polite"
        className="min-h-6 flex-row items-center"
      >
        {saved ? (
          <AnimatedView
            entering={FadeInDown.duration(400).reduceMotion(
              ReduceMotion.System
            )}
            className="flex-row items-center gap-1.5"
          >
            <Icon
              as={CheckIcon}
              weight="bold"
              className="size-4 text-green-600 dark:text-green-400"
            />
            <Text className="text-sm">{saved} saved.</Text>
          </AnimatedView>
        ) : null}
      </View>
    </View>
  )
}

function SettingsAccount({
  onSave,
}: {
  onSave?: (field: "email" | "phone", value: string) => void
}) {
  const [section, setSection] = React.useState<string | null>("security")
  const scroll = useHeaderScroll()
  const current = ITEMS.find((item) => item.value === section)

  return (
    <Screen edges={["top", "right", "left"]}>
      <Header scroll={scroll}>
        {current ? (
          <HeaderBack onPress={() => setSection(null)}>Account</HeaderBack>
        ) : null}
        <HeaderTitle>{current ? current.label : "Account"}</HeaderTitle>
      </Header>
      <HeaderScrollView key={section ?? "account"} scroll={scroll}>
        {current ? (
          <>
            <HeaderLargeTitle>{current.label}</HeaderLargeTitle>
            <AnimatedView
              entering={FadeInDown.duration(600).reduceMotion(
                ReduceMotion.System
              )}
              className="px-4 pt-2"
            >
              {current.value === "security" ? (
                <Security onSave={onSave} />
              ) : (
                <Text className="text-lg text-muted-foreground">
                  This section isn't part of the sample. Choose Sign-In &
                  Security.
                </Text>
              )}
            </AnimatedView>
          </>
        ) : (
          <>
            <HeaderLargeTitle className="gap-2 pb-4">
              <Avatar size="xl" className="size-18">
                <AvatarFallback className="bg-indigo-500">JR</AvatarFallback>
              </Avatar>
              <Text role="heading" className="text-2xl font-semibold">
                Jamie Rivera
              </Text>
              <Text className="text-sm text-muted-foreground">
                jamie@example.com
              </Text>
            </HeaderLargeTitle>
            <View className="px-4">
              <ItemGroup variant="inset">
                {ITEMS.map((item, index) => (
                  <React.Fragment key={item.value}>
                    {index > 0 ? <ItemSeparator className="ms-12.5" /> : null}
                    <Item
                      size="sm"
                      onPress={() => setSection(item.value)}
                      className="min-h-11 gap-3.5 py-2.5"
                    >
                      <ItemMedia>
                        <Icon as={item.icon} className="size-5 text-primary" />
                      </ItemMedia>
                      <ItemContent>
                        <ItemTitle className="font-normal">
                          {item.label}
                        </ItemTitle>
                      </ItemContent>
                      <ItemActions>
                        <Icon
                          as={CaretRightIcon}
                          weight="bold"
                          className={cn(
                            "size-3.5 text-muted-foreground",
                            I18nManager.isRTL && "rotate-180"
                          )}
                        />
                      </ItemActions>
                    </Item>
                  </React.Fragment>
                ))}
              </ItemGroup>
            </View>
          </>
        )}
        <View className="h-8" />
      </HeaderScrollView>
    </Screen>
  )
}

export { SettingsAccount }
```

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

public struct SettingsAccount: View {
  struct Section: Identifiable {
    let id: String
    let label: LocalizedStringKey
    let icon: Image
  }

  static var sections: [Section] {
    [
      Section(id: "personal", label: "Personal Information", icon: Ph.user.regular),
      Section(id: "security", label: "Sign-In & Security", icon: Ph.key.regular),
      Section(id: "payment", label: "Payment & Shipping", icon: Ph.creditCard.regular),
      Section(id: "subscriptions", label: "Subscriptions", icon: Ph.cloud.regular),
      Section(id: "family", label: "Family Sharing", icon: Ph.users.regular),
      Section(id: "devices", label: "Devices", icon: Ph.deviceMobile.regular),
      Section(id: "privacy", label: "Privacy", icon: Ph.shield.regular),
    ]
  }

  private var onSave: ((String, String) -> Void)?
  @State private var section: String?
  @State private var editing: String?
  @Environment(\.accessibilityReduceMotion) private var reduceMotion

  public init(onSave: ((_ field: String, _ value: String) -> Void)? = nil) {
    self.init(section: "security")
    self.onSave = onSave
  }

  init(section: String?, editing: String? = nil) {
    _section = State(initialValue: section)
    _editing = State(initialValue: editing)
  }

  public var body: some View {
    Screen {
      ZStack {
        if let current = Self.sections.first(where: { $0.id == section }) {
          detail(current)
            .transition(reduceMotion ? .opacity : .move(edge: .trailing))
            .zIndex(1)
        } else {
          root
            .transition(reduceMotion ? .opacity : .move(edge: .leading).combined(with: .opacity))
        }
      }
    }
    .animation(reduceMotion ? nil : .timingCurve(0.32, 0.72, 0, 1, duration: 0.45), value: section)
  }

  private var root: some View {
    KeyboardScrollView {
      VStack(spacing: 24) {
        VStack(spacing: 8) {
          Avatar(size: .xl) {
            Text(verbatim: "JR")
              .scaledFont(.xl, weight: .semibold)
              .foregroundStyle(EsuissColor.primaryForeground)
              .frame(maxWidth: .infinity, maxHeight: .infinity)
              .background(
                Palette.indigo500,
                in: Circle()
              )
          }
          Text(verbatim: "Jamie Rivera")
            .scaledTextStyle(.xl, weight: .bold)
          Text(verbatim: "jamie@example.com")
            .scaledFont(.sm)
            .foregroundStyle(EsuissColor.mutedForeground)
        }
        .frame(maxWidth: .infinity)
        .accessibilityElement(children: .combine)
        ItemGroup(.inset) {
          ForEach(Array(Self.sections.enumerated()), id: \.element.id) { index, item in
            if index > 0 {
              Separator().padding(.leading, 54)
            }
            Item(size: .sm, action: { section = item.id }) {
              ItemMedia {
                item.icon
                  .frame(width: 22, height: 22)
                  .foregroundStyle(EsuissColor.primary)
              }
              ItemContent {
                ItemTitle {
                  Text(item.label)
                    .fontWeight(.regular)
                }
              }
              ItemActions {
                Ph.caretRight.bold
                  .frame(width: 14, height: 14)
                  .foregroundStyle(EsuissColor.mutedForeground)
                  .flipsForRightToLeftLayoutDirection(true)
              }
            }
          }
        }
      }
      .padding(.horizontal, 16)
      .padding(.top, 40)
      .padding(.bottom, 24)
    }
  }

  private func detail(_ current: Section) -> some View {
    HeaderScrollView {
      HeaderLargeTitle(current.label)
      Group {
        if current.id == "security" {
          SettingsAccountSecurity(editing: $editing, onSave: onSave)
        } else {
          Text("This section isn't part of the sample. Choose Sign-In & Security.")
            .scaledTextStyle(.lg)
            .foregroundStyle(EsuissColor.mutedForeground)
            .fixedSize(horizontal: false, vertical: true)
        }
      }
      .padding(.horizontal, 16)
      .padding(.top, 8)
      .padding(.bottom, 24)
    } header: {
      Header {
        HeaderTitle(current.label)
      } leading: {
        HeaderBack("Account") {
          editing = nil
          section = nil
        }
      }
    }
    .background(EsuissColor.background)
  }
}

private struct SettingsAccountSecurity: View {
  @Binding var editing: String?
  let onSave: ((String, String) -> Void)?

  @State private var email = "jamie@example.com"
  @State private var phone = "+1 (415) 555-0142"
  @State private var passkeys = true
  @State private var saved: LocalizedStringKey?
  @State private var savedCount = 0

  var body: some View {
    VStack(alignment: .leading, spacing: 14) {
      SettingsAccountCard(
        title: "Email & phone",
        value: $email,
        editing: editing == "email",
        onEdit: { editing = "email" },
        onCancel: { editing = nil },
        onSave: {
          editing = nil
          onSave?("email", email)
          announce("Email & phone saved.")
        }
      )
      SettingsAccountCard(
        title: "Trusted phone number",
        value: $phone,
        editing: editing == "phone",
        onEdit: { editing = "phone" },
        onCancel: { editing = nil },
        onSave: {
          editing = nil
          onSave?("phone", phone)
          announce("Trusted phone number saved.")
        }
      )
      HStack(spacing: 12) {
        VStack(alignment: .leading, spacing: 3) {
          Text("Passkeys")
            .scaledFont(.lg, weight: .semibold)
          Text(passkeys ? "On for 3 devices" : "Off")
            .scaledFont(.base)
            .foregroundStyle(EsuissColor.mutedForeground)
        }
        .frame(maxWidth: .infinity, alignment: .leading)
        Toggle(isOn: $passkeys) { Text("Passkeys") }
          .labelsHidden()
          .accessibilityLabel(Text("Passkeys"))
          .toggleStyle(SwitchStyle())
      }
      .modifier(SettingsAccountCardSurface())
      HStack(spacing: 12) {
        VStack(alignment: .leading, spacing: 3) {
          Text("Two-factor authentication")
            .scaledFont(.lg, weight: .semibold)
          Text("Codes are sent to your trusted devices.")
            .scaledTextStyle(.base)
            .foregroundStyle(EsuissColor.mutedForeground)
            .fixedSize(horizontal: false, vertical: true)
        }
        .frame(maxWidth: .infinity, alignment: .leading)
        Text("On")
          .scaledFont(.sm, weight: .semibold)
          .foregroundStyle(Color.adaptive(light: Palette.green600, dark: Palette.green400))
      }
      .accessibilityElement(children: .combine)
      .modifier(SettingsAccountCardSurface())
      HStack(spacing: 6) {
        if let saved {
          Ph.check.bold
            .frame(width: 16, height: 16)
            .foregroundStyle(Color.adaptive(light: Palette.green600, dark: Palette.green400))
          Text(saved)
            .scaledFont(.sm)
        }
      }
      .frame(minHeight: 24)
      .transition(.opacity.combined(with: .offset(y: 6)))
      .task(id: savedCount) {
        guard saved != nil else { return }
        try? await Task.sleep(for: .milliseconds(2200))
        guard !Task.isCancelled else { return }
        withAnimation { saved = nil }
      }
    }
  }

  private func announce(_ message: LocalizedStringKey) {
    withAnimation(.easeOut(duration: 0.4)) { saved = message }
    savedCount += 1
  }
}

private struct SettingsAccountCard: View {
  let title: LocalizedStringKey
  @Binding var value: String
  let editing: Bool
  let onEdit: () -> Void
  let onCancel: () -> Void
  let onSave: () -> Void

  @State private var draft = ""
  @FocusState private var focused: Bool
  @Environment(\.accessibilityReduceMotion) private var reduceMotion

  var body: some View {
    VStack(alignment: .leading, spacing: 0) {
      HStack(spacing: 12) {
        VStack(alignment: .leading, spacing: 3) {
          Text(title)
            .scaledFont(.lg, weight: .semibold)
            .accessibilityAddTraits(.isHeader)
          if !editing {
            Text(verbatim: value)
              .scaledFont(.base)
              .foregroundStyle(EsuissColor.mutedForeground)
          }
        }
        .frame(maxWidth: .infinity, alignment: .leading)
        if !editing {
          Button("Edit ›", action: onEdit)
            .buttonStyle(SettingsAccountLinkStyle())
            .accessibilityLabel(Text("Edit \(Text(title))"))
        }
      }
      if editing {
        VStack(alignment: .leading, spacing: 12) {
          TextField(title, text: $draft)
            .textFieldStyle(.input)
            .focused($focused)
            .submitLabel(.done)
            .onSubmit(save)
          HStack(spacing: 10) {
            Button("Save", action: save)
              .buttonStyle(.variant(.default, size: .sm))
            Button("Cancel", action: onCancel)
              .buttonStyle(.variant(.outline, size: .sm))
          }
        }
        .padding(.top, 14)
        .transition(.opacity.combined(with: .move(edge: .top)))
      }
    }
    .modifier(SettingsAccountCardSurface())
    .animation(reduceMotion ? nil : .timingCurve(0.32, 0.72, 0, 1, duration: 0.4), value: editing)
    .onChange(of: editing, initial: true) { _, open in
      guard open else { return }
      draft = value
      focused = true
    }
  }

  private func save() {
    value = draft
    onSave()
  }
}

private struct SettingsAccountCardSurface: ViewModifier {
  func body(content: Content) -> some View {
    content
      .foregroundStyle(EsuissColor.foreground)
      .padding(.horizontal, 20)
      .padding(.vertical, 18)
      .frame(maxWidth: .infinity, alignment: .leading)
      .background(EsuissColor.muted, in: RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous))
  }
}

private struct SettingsAccountLinkStyle: 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/SettingsAccount.kt"
package com.esuiss.ui.blocks

import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.tween
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.togetherWith
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.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateMapOf
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.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.semantics.LiveRegionMode
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.liveRegion
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Bold
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.bold.CaretRight
import com.adamglin.phosphoricons.bold.Check
import com.adamglin.phosphoricons.regular.Cloud
import com.adamglin.phosphoricons.regular.CreditCard
import com.adamglin.phosphoricons.regular.DeviceMobile
import com.adamglin.phosphoricons.regular.Key
import com.adamglin.phosphoricons.regular.Shield
import com.adamglin.phosphoricons.regular.User
import com.adamglin.phosphoricons.regular.Users
import com.esuiss.ui.components.Avatar
import com.esuiss.ui.components.AvatarSize
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonSize
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Header
import com.esuiss.ui.components.HeaderBack
import com.esuiss.ui.components.HeaderLargeTitle
import com.esuiss.ui.components.HeaderTitle
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Input
import com.esuiss.ui.components.Item
import com.esuiss.ui.components.ItemActions
import com.esuiss.ui.components.ItemContent
import com.esuiss.ui.components.ItemGroup
import com.esuiss.ui.components.ItemGroupVariant
import com.esuiss.ui.components.ItemMedia
import com.esuiss.ui.components.ItemSeparator
import com.esuiss.ui.components.ItemTitle
import com.esuiss.ui.components.KeyboardScrollView
import com.esuiss.ui.components.Screen
import com.esuiss.ui.components.Switch
import com.esuiss.ui.components.rememberHeaderState
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Palette
import com.esuiss.ui.theme.Typography
import kotlinx.coroutines.delay

private val RevealEasing = CubicBezierEasing(0.32f, 0.72f, 0f, 1f)

private class AccountSection(val value: String, val label: String, val icon: ImageVector)

private val Sections = listOf(
    AccountSection("personal", "Personal Information", PhosphorIcons.Regular.User),
    AccountSection("security", "Sign-In & Security", PhosphorIcons.Regular.Key),
    AccountSection("payment", "Payment & Shipping", PhosphorIcons.Regular.CreditCard),
    AccountSection("subscriptions", "Subscriptions", PhosphorIcons.Regular.Cloud),
    AccountSection("family", "Family Sharing", PhosphorIcons.Regular.Users),
    AccountSection("devices", "Devices", PhosphorIcons.Regular.DeviceMobile),
    AccountSection("privacy", "Privacy", PhosphorIcons.Regular.Shield),
)

private class EditableField(val field: String, val title: String)

private val EditableFields = listOf(
    EditableField("email", "Email & phone"),
    EditableField("phone", "Trusted phone number"),
)

@Composable
fun SettingsAccount(
    modifier: Modifier = Modifier,
    onSave: (field: String, value: String) -> Unit = { _, _ -> },
) {
    var section by rememberSaveable { mutableStateOf<String?>("security") }
    Screen(modifier) {
        AnimatedContent(
            targetState = section,
            transitionSpec = {
                val forward = targetState != null
                (fadeIn(tween(300)) + slideInHorizontally(tween(450, easing = RevealEasing)) { if (forward) it / 4 else -it / 4 }) togetherWith
                    fadeOut(tween(150))
            },
            label = "section",
        ) { current ->
            if (current == null) {
                AccountHome { section = it }
            } else {
                SectionPage(
                    section = Sections.first { it.value == current },
                    onBack = { section = null },
                    onSave = onSave,
                )
            }
        }
    }
}

@Composable
private fun AccountHome(onSelect: (String) -> Unit) {
    val colors = EsuissTheme.colors
    val header = rememberHeaderState()
    Column(
        Modifier
            .fillMaxSize()
            .nestedScroll(header.nestedScrollConnection),
    ) {
        Header(state = header) {
            HeaderTitle("Account")
            HeaderLargeTitle("Account")
        }
        AccountList(Modifier.weight(1f), onSelect)
    }
}

@Composable
private fun AccountList(modifier: Modifier, onSelect: (String) -> Unit) {
    val colors = EsuissTheme.colors
    Column(
        modifier
            .fillMaxSize()
            .verticalScroll(rememberScrollState())
            .padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(20.dp),
    ) {
        Column(
            modifier = Modifier.padding(horizontal = 8.dp),
            verticalArrangement = Arrangement.spacedBy(8.dp),
        ) {
            Avatar(
                modifier = Modifier.size(72.dp),
                size = AvatarSize.Xl,
                fallback = {
                    Box(
                        Modifier
                            .fillMaxSize()
                            .background(Palette.indigo500, CircleShape),
                        contentAlignment = Alignment.Center,
                    ) {
                        BasicText("JR", style = Typography.xl2.copy(color = colors.primaryForeground, fontWeight = FontWeight.SemiBold))
                    }
                },
            )
            BasicText(
                "Jamie Rivera",
                modifier = Modifier.semantics { heading() },
                style = Typography.xl2.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold),
            )
            BasicText("jamie@example.com", style = Typography.sm.copy(color = colors.mutedForeground))
        }
        ItemGroup(variant = ItemGroupVariant.Inset) {
            Sections.forEachIndexed { index, section ->
                if (index > 0) ItemSeparator(Modifier.padding(start = 34.dp))
                Item(onClick = { onSelect(section.value) }) {
                    ItemMedia { Icon(section.icon, contentDescription = null, size = 20.dp, tint = colors.primary) }
                    ItemContent { ItemTitle(section.label) }
                    ItemActions {
                        Icon(PhosphorIcons.Bold.CaretRight, contentDescription = null, size = 14.dp, tint = colors.mutedForeground)
                    }
                }
            }
        }
    }
}

@Composable
private fun SectionPage(
    section: AccountSection,
    onBack: () -> Unit,
    onSave: (String, String) -> Unit,
) {
    val colors = EsuissTheme.colors
    val header = rememberHeaderState()
    Column(
        Modifier
            .fillMaxSize()
            .nestedScroll(header.nestedScrollConnection),
    ) {
        Header(state = header) {
            HeaderBack(onClick = onBack, text = "Account")
            HeaderTitle(section.label)
            HeaderLargeTitle(section.label)
        }
        KeyboardScrollView(
            modifier = Modifier.weight(1f),
            contentPadding = PaddingValues(start = 20.dp, end = 20.dp, top = 8.dp, bottom = 32.dp),
            verticalArrangement = Arrangement.spacedBy(14.dp),
        ) {
            if (section.value == "security") {
                Security(onSave)
            } else {
                BasicText(
                    "This section isn't part of the sample. Choose Sign-In & Security.",
                    style = Typography.lg.copy(color = colors.mutedForeground),
                )
            }
        }
    }
}

@Composable
private fun Security(onSave: (String, String) -> Unit) {
    val colors = EsuissTheme.colors
    val values = remember { mutableStateMapOf("email" to "jamie@example.com", "phone" to "+1 (415) 555-0142") }
    var editing by rememberSaveable { mutableStateOf<String?>(null) }
    var passkeys by rememberSaveable { mutableStateOf(true) }
    var saved by remember { mutableStateOf<String?>(null) }

    LaunchedEffect(saved) {
        if (saved == null) return@LaunchedEffect
        delay(2200)
        saved = null
    }

    Column(verticalArrangement = Arrangement.spacedBy(14.dp)) {
        EditableFields.forEach { card ->
            EditableCard(
                title = card.title,
                value = values.getValue(card.field),
                editing = editing == card.field,
                onEdit = { editing = card.field },
                onCancel = { editing = null },
                onSave = { value ->
                    values[card.field] = value
                    editing = null
                    saved = card.title
                    onSave(card.field, value)
                },
            )
        }
        Card {
            Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp)) {
                BasicText("Passkeys", style = Typography.lg.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold))
                BasicText(if (passkeys) "On for 3 devices" else "Off", style = Typography.base.copy(color = colors.mutedForeground))
            }
            Switch(
                checked = passkeys,
                onCheckedChange = {
                    passkeys = it
                },
            )
        }
        Card {
            Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp)) {
                BasicText("Two-factor authentication", style = Typography.lg.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold))
                BasicText("Codes are sent to your trusted devices.", style = Typography.base.copy(color = colors.mutedForeground))
            }
            BasicText("On", style = Typography.sm.copy(color = if (colors.isDark) Palette.green400 else Palette.green600, fontWeight = FontWeight.SemiBold))
        }
        Column(
            Modifier
                .heightIn(min = 24.dp)
                .semantics { liveRegion = LiveRegionMode.Polite },
        ) {
            AnimatedVisibility(
                visible = saved != null,
                enter = fadeIn(tween(800)) + slideInVertically(tween(800)) { it / 4 },
                exit = fadeOut(tween(200)),
            ) {
                Row(horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically) {
                    Icon(PhosphorIcons.Bold.Check, contentDescription = null, size = 16.dp, tint = if (colors.isDark) Palette.green400 else Palette.green600)
                    BasicText("${saved ?: ""} saved.", style = Typography.sm.copy(color = colors.foreground))
                }
            }
        }
    }
}

@Composable
private fun Card(content: @Composable RowScope.() -> Unit) {
    val shape = RoundedCornerShape(18.dp)
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .clip(shape)
            .background(EsuissTheme.colors.muted, shape)
            .padding(horizontal = 20.dp, vertical = 18.dp),
        horizontalArrangement = Arrangement.spacedBy(12.dp),
        verticalAlignment = Alignment.CenterVertically,
        content = content,
    )
}

@Composable
private fun EditableCard(
    title: String,
    value: String,
    editing: Boolean,
    onEdit: () -> Unit,
    onCancel: () -> Unit,
    onSave: (String) -> Unit,
) {
    val colors = EsuissTheme.colors
    var draft by remember(editing, value) { mutableStateOf(value) }
    val focus = remember { FocusRequester() }
    LaunchedEffect(editing) {
        if (!editing) return@LaunchedEffect
        delay(300)
        runCatching { focus.requestFocus() }
    }
    val shape = RoundedCornerShape(18.dp)
    Column(
        Modifier
            .fillMaxWidth()
            .clip(shape)
            .background(colors.muted, shape)
            .padding(horizontal = 20.dp, vertical = 18.dp),
    ) {
        Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) {
            Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp)) {
                BasicText(
                    title,
                    modifier = Modifier.semantics { heading() },
                    style = Typography.lg.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold),
                )
                if (!editing) BasicText(value, style = Typography.base.copy(color = colors.mutedForeground))
            }
            if (!editing) {
                val interaction = remember { MutableInteractionSource() }
                val pressed by interaction.collectIsPressedAsState()
                BasicText(
                    "Edit ›",
                    modifier = Modifier
                        .graphicsLayer { alpha = if (pressed) 0.8f else 1f }
                        .clickable(
                            interactionSource = interaction,
                            indication = null,
                            role = Role.Button,
                            onClickLabel = "Edit ${title.lowercase()}",
                            onClick = onEdit,
                        )
                        .padding(vertical = 8.dp),
                    style = Typography.base.copy(color = colors.primary),
                )
            }
        }
        AnimatedVisibility(
            visible = editing,
            enter = expandVertically(tween(400, easing = RevealEasing)),
            exit = shrinkVertically(tween(400, easing = RevealEasing)),
        ) {
            Column(
                modifier = Modifier.padding(top = 14.dp),
                verticalArrangement = Arrangement.spacedBy(12.dp),
            ) {
                Input(
                    value = draft,
                    onValueChange = { draft = it },
                    modifier = Modifier.focusRequester(focus),
                    placeholder = title,
                )
                Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
                    Button("Save", { onSave(draft) }, size = ButtonSize.Sm)
                    Button("Cancel", onCancel, variant = ButtonVariant.Outline, size = ButtonSize.Sm)
                }
            }
        }
    }
}
```

The main page shows an `Avatar` and name above an inset `ItemGroup` of account sections; tapping one pushes it into the same `Header` with a back button. The Sign-In & Security page has cards for email and phone that open an inline `Input` with Save and Cancel, a passkeys `Switch`, and a confirmation that appears after saving. Other sections show a placeholder.

## Installation

**React Native**

```bash title="Terminal"
npx shadcn@latest add @esuiss-rn/settings-account
```

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 settings-account
```

**SwiftUI**

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

**Jetpack Compose**

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

The CLI also adds what it builds on: [Avatar](https://ui.suiss.app/docs/components/avatar), [Button](https://ui.suiss.app/docs/components/button), [Header](https://ui.suiss.app/docs/components/header), [Icon](https://ui.suiss.app/docs/components/icon), [Input](https://ui.suiss.app/docs/components/input), [Item](https://ui.suiss.app/docs/components/item), [Screen](https://ui.suiss.app/docs/components/screen), [Switch](https://ui.suiss.app/docs/components/switch), [Text](https://ui.suiss.app/docs/components/text). On React Native it also adds the animated view `components/ui/animated-view.tsx`.

### Manual

**React Native**

**Uniwind**

1. Install the dependencies:

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

2. Add the pieces it builds on: [Avatar](https://ui.suiss.app/docs/components/avatar), [Button](https://ui.suiss.app/docs/components/button), [Header](https://ui.suiss.app/docs/components/header), [Icon](https://ui.suiss.app/docs/components/icon), [Input](https://ui.suiss.app/docs/components/input), [Item](https://ui.suiss.app/docs/components/item), [Screen](https://ui.suiss.app/docs/components/screen), [Switch](https://ui.suiss.app/docs/components/switch), [Text](https://ui.suiss.app/docs/components/text), and the animated view `components/ui/animated-view.tsx` from the next step.

3. Copy the animated view into `components/ui/animated-view.tsx`:

- `uniwind/components/ui/animated-view.tsx`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/uniwind/components/ui/animated-view.tsx)

4. Copy the source into `components/blocks/settings-account.tsx`:

- `uniwind/components/blocks/settings-account.tsx`: shown above

5. 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-reanimated react-native-svg react-native-worklets
```

2. Add the pieces it builds on: [Avatar](https://ui.suiss.app/docs/components/avatar), [Button](https://ui.suiss.app/docs/components/button), [Header](https://ui.suiss.app/docs/components/header), [Icon](https://ui.suiss.app/docs/components/icon), [Input](https://ui.suiss.app/docs/components/input), [Item](https://ui.suiss.app/docs/components/item), [Screen](https://ui.suiss.app/docs/components/screen), [Switch](https://ui.suiss.app/docs/components/switch), [Text](https://ui.suiss.app/docs/components/text), and the animated view `components/ui/animated-view.tsx` from the next step.

3. Copy the animated view into `components/ui/animated-view.tsx`:

- `nativewind/components/ui/animated-view.tsx`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/nativewind/components/ui/animated-view.tsx)

4. Copy the source into `components/blocks/settings-account.tsx`:

```tsx title="nativewind/components/blocks/settings-account.tsx"
import { cn } from "cn"
import {
  CaretRightIcon,
  CheckIcon,
  CloudIcon,
  CreditCardIcon,
  DeviceMobileIcon,
  KeyIcon,
  type Icon as PhosphorIcon,
  ShieldIcon,
  UserIcon,
  UsersIcon,
} from "phosphor-react-native"
import * as React from "react"
import { I18nManager, Pressable, View } from "react-native"
import { FadeIn, FadeInDown, ReduceMotion } from "react-native-reanimated"
import { AnimatedView } from "@/components/ui/animated-view"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
  Header,
  HeaderBack,
  HeaderLargeTitle,
  HeaderScrollView,
  HeaderTitle,
  useHeaderScroll,
} from "@/components/ui/header"
import { Icon } from "@/components/ui/icon"
import { Input } from "@/components/ui/input"
import {
  Item,
  ItemActions,
  ItemContent,
  ItemGroup,
  ItemMedia,
  ItemSeparator,
  ItemTitle,
} from "@/components/ui/item"
import { Screen } from "@/components/ui/screen"
import { Switch } from "@/components/ui/switch"
import { Text } from "@/components/ui/text"

const ITEMS: { value: string; label: string; icon: PhosphorIcon }[] = [
  { value: "personal", label: "Personal Information", icon: UserIcon },
  { value: "security", label: "Sign-In & Security", icon: KeyIcon },
  { value: "payment", label: "Payment & Shipping", icon: CreditCardIcon },
  { value: "subscriptions", label: "Subscriptions", icon: CloudIcon },
  { value: "family", label: "Family Sharing", icon: UsersIcon },
  { value: "devices", label: "Devices", icon: DeviceMobileIcon },
  { value: "privacy", label: "Privacy", icon: ShieldIcon },
]

const CARD_CLASS = "rounded-[1.125rem] bg-muted px-5 py-[1.125rem]"

type Editable = "email" | "phone"

function EditableCard({
  title,
  value,
  editing,
  onEdit,
  onCancel,
  onSave,
}: {
  title: string
  value: string
  editing: boolean
  onEdit: () => void
  onCancel: () => void
  onSave: (value: string) => void
}) {
  const [draft, setDraft] = React.useState(value)

  React.useEffect(() => {
    if (editing) setDraft(value)
  }, [editing, value])

  return (
    <View className={CARD_CLASS}>
      <View className="flex-row items-center gap-3">
        <View className="flex-1 gap-[0.1875rem]">
          <Text role="heading" className="text-lg font-semibold">
            {title}
          </Text>
          {editing ? null : (
            <Text className="text-base text-muted-foreground">{value}</Text>
          )}
        </View>
        {editing ? null : (
          <Pressable
            role="button"
            hitSlop={8}
            onPress={onEdit}
            accessibilityLabel={`Edit ${title.toLowerCase()}`}
            className="active:opacity-60"
          >
            <Text className="text-base text-primary">Edit ›</Text>
          </Pressable>
        )}
      </View>
      {editing ? (
        <AnimatedView
          entering={FadeIn.duration(300).reduceMotion(ReduceMotion.System)}
          className="gap-3 pt-3.5"
        >
          <Input
            autoFocus
            value={draft}
            onChangeText={setDraft}
            accessibilityLabel={title}
            returnKeyType="done"
            onSubmitEditing={() => onSave(draft)}
            className={(state) =>
              cn(
                "h-12 rounded-xl bg-background text-[18px]",
                state.focused ? "border-ring ring ring-ring-50" : "border-input"
              )
            }
          />
          <View className="flex-row gap-2.5">
            <Button size="sm" onPress={() => onSave(draft)}>
              <Text>Save</Text>
            </Button>
            <Button size="sm" variant="outline" onPress={onCancel}>
              <Text>Cancel</Text>
            </Button>
          </View>
        </AnimatedView>
      ) : null}
    </View>
  )
}

function Security({
  onSave,
}: {
  onSave?: (field: Editable, value: string) => void
}) {
  const [values, setValues] = React.useState<Record<Editable, string>>({
    email: "jamie@example.com",
    phone: "+1 (415) 555-0142",
  })
  const [editing, setEditing] = React.useState<Editable | null>(null)
  const [passkeys, setPasskeys] = React.useState(true)
  const [saved, setSaved] = React.useState<string | null>(null)

  React.useEffect(() => {
    if (!saved) return
    const timer = setTimeout(() => setSaved(null), 2200)
    return () => clearTimeout(timer)
  }, [saved])

  const cards: { field: Editable; title: string }[] = [
    { field: "email", title: "Email & phone" },
    { field: "phone", title: "Trusted phone number" },
  ]

  return (
    <View className="gap-3.5">
      {cards.map((card) => (
        <EditableCard
          key={card.field}
          title={card.title}
          value={values[card.field]}
          editing={editing === card.field}
          onEdit={() => setEditing(card.field)}
          onCancel={() => setEditing(null)}
          onSave={(value) => {
            onSave?.(card.field, value)
            setValues((current) => ({ ...current, [card.field]: value }))
            setEditing(null)
            setSaved(card.title)
          }}
        />
      ))}
      <View className={cn(CARD_CLASS, "flex-row items-center gap-3")}>
        <View className="flex-1 gap-[0.1875rem]">
          <Text
            nativeID="settings-account-passkeys"
            className="text-lg font-semibold"
          >
            Passkeys
          </Text>
          <Text className="text-base text-muted-foreground">
            {passkeys ? "On for 3 devices" : "Off"}
          </Text>
        </View>
        <Switch
          checked={passkeys}
          onCheckedChange={setPasskeys}
          accessibilityLabelledBy="settings-account-passkeys"
        />
      </View>
      <View className={cn(CARD_CLASS, "flex-row items-center gap-3")}>
        <View className="flex-1 gap-[0.1875rem]">
          <Text role="heading" className="text-lg font-semibold">
            Two-factor authentication
          </Text>
          <Text className="text-base text-muted-foreground">
            Codes are sent to your trusted devices.
          </Text>
        </View>
        <Text className="text-sm font-semibold text-green-600 dark:text-green-400">
          On
        </Text>
      </View>
      <View
        accessibilityLiveRegion="polite"
        className="min-h-6 flex-row items-center"
      >
        {saved ? (
          <AnimatedView
            entering={FadeInDown.duration(400).reduceMotion(
              ReduceMotion.System
            )}
            className="flex-row items-center gap-1.5"
          >
            <Icon
              as={CheckIcon}
              weight="bold"
              className="size-4 text-green-600 dark:text-green-400"
            />
            <Text className="text-sm">{saved} saved.</Text>
          </AnimatedView>
        ) : null}
      </View>
    </View>
  )
}

function SettingsAccount({
  onSave,
}: {
  onSave?: (field: "email" | "phone", value: string) => void
}) {
  const [section, setSection] = React.useState<string | null>("security")
  const scroll = useHeaderScroll()
  const current = ITEMS.find((item) => item.value === section)

  return (
    <Screen edges={["top", "right", "left"]}>
      <Header scroll={scroll}>
        {current ? (
          <HeaderBack onPress={() => setSection(null)}>Account</HeaderBack>
        ) : null}
        <HeaderTitle>{current ? current.label : "Account"}</HeaderTitle>
      </Header>
      <HeaderScrollView key={section ?? "account"} scroll={scroll}>
        {current ? (
          <>
            <HeaderLargeTitle>{current.label}</HeaderLargeTitle>
            <AnimatedView
              entering={FadeInDown.duration(600).reduceMotion(
                ReduceMotion.System
              )}
              className="px-4 pt-2"
            >
              {current.value === "security" ? (
                <Security onSave={onSave} />
              ) : (
                <Text className="text-lg text-muted-foreground">
                  This section isn't part of the sample. Choose Sign-In &
                  Security.
                </Text>
              )}
            </AnimatedView>
          </>
        ) : (
          <>
            <HeaderLargeTitle className="gap-2 pb-4">
              <Avatar size="xl" className="size-[4.5rem]">
                <AvatarFallback className="bg-indigo-500">JR</AvatarFallback>
              </Avatar>
              <Text role="heading" className="text-2xl font-semibold">
                Jamie Rivera
              </Text>
              <Text className="text-sm text-muted-foreground">
                jamie@example.com
              </Text>
            </HeaderLargeTitle>
            <View className="px-4">
              <ItemGroup variant="inset">
                {ITEMS.map((item, index) => (
                  <React.Fragment key={item.value}>
                    {index > 0 ? (
                      <ItemSeparator className="ms-[3.125rem]" />
                    ) : null}
                    <Item
                      size="sm"
                      onPress={() => setSection(item.value)}
                      className="min-h-11 gap-3.5 py-2.5"
                    >
                      <ItemMedia>
                        <Icon as={item.icon} className="size-5 text-primary" />
                      </ItemMedia>
                      <ItemContent>
                        <ItemTitle className="font-normal">
                          {item.label}
                        </ItemTitle>
                      </ItemContent>
                      <ItemActions>
                        <Icon
                          as={CaretRightIcon}
                          weight="bold"
                          className={cn(
                            "size-3.5 text-muted-foreground",
                            I18nManager.isRTL && "rotate-180"
                          )}
                        />
                      </ItemActions>
                    </Item>
                  </React.Fragment>
                ))}
              </ItemGroup>
            </View>
          </>
        )}
        <View className="h-8" />
      </HeaderScrollView>
    </Screen>
  )
}

export { SettingsAccount }
```

5. Update the import paths to match your project.

**StyleSheet**

1. Install the dependencies:

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

2. Add the pieces it builds on: [Avatar](https://ui.suiss.app/docs/components/avatar), [Button](https://ui.suiss.app/docs/components/button), [Header](https://ui.suiss.app/docs/components/header), [Icon](https://ui.suiss.app/docs/components/icon), [Input](https://ui.suiss.app/docs/components/input), [Item](https://ui.suiss.app/docs/components/item), [Screen](https://ui.suiss.app/docs/components/screen), [Switch](https://ui.suiss.app/docs/components/switch), [Text](https://ui.suiss.app/docs/components/text), and the animated view `components/ui/animated-view.tsx` from the next step.

3. Copy the animated view into `components/ui/animated-view.tsx`:

- `stylesheet/components/ui/animated-view.tsx`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/stylesheet/components/ui/animated-view.tsx)

4. Copy the source into `components/blocks/settings-account.tsx`:

```tsx title="stylesheet/components/blocks/settings-account.tsx"
import {
  CaretRightIcon,
  CheckIcon,
  CloudIcon,
  CreditCardIcon,
  DeviceMobileIcon,
  KeyIcon,
  type Icon as PhosphorIcon,
  ShieldIcon,
  UserIcon,
  UsersIcon,
} from "phosphor-react-native"
import * as React from "react"
import { I18nManager, Pressable, StyleSheet, View } from "react-native"
import { FadeIn, FadeInDown, ReduceMotion } from "react-native-reanimated"
import { AnimatedView } from "@/components/ui/animated-view"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
  Header,
  HeaderBack,
  HeaderLargeTitle,
  HeaderScrollView,
  HeaderTitle,
  useHeaderScroll,
} from "@/components/ui/header"
import { Icon } from "@/components/ui/icon"
import { Input } from "@/components/ui/input"
import {
  Item,
  ItemActions,
  ItemContent,
  ItemGroup,
  ItemMedia,
  ItemSeparator,
  ItemTitle,
} from "@/components/ui/item"
import { Screen } from "@/components/ui/screen"
import { Switch } from "@/components/ui/switch"
import { Text } from "@/components/ui/text"
import { fontSize, palette, radius, useTheme } from "@/components/ui/theme"

const ITEMS: { value: string; label: string; icon: PhosphorIcon }[] = [
  { value: "personal", label: "Personal Information", icon: UserIcon },
  { value: "security", label: "Sign-In & Security", icon: KeyIcon },
  { value: "payment", label: "Payment & Shipping", icon: CreditCardIcon },
  { value: "subscriptions", label: "Subscriptions", icon: CloudIcon },
  { value: "family", label: "Family Sharing", icon: UsersIcon },
  { value: "devices", label: "Devices", icon: DeviceMobileIcon },
  { value: "privacy", label: "Privacy", icon: ShieldIcon },
]

const styles = StyleSheet.create({
  card: { borderRadius: 18, paddingHorizontal: 20, paddingVertical: 18 },
  cardRow: { flexDirection: "row", alignItems: "center", gap: 12 },
  cardText: { flex: 1, gap: 3 },
  cardTitle: { ...fontSize.lg, fontWeight: "600" },
  editor: { gap: 12, paddingTop: 14 },
  input: {
    fontSize: fontSize.lg.fontSize,
    height: 48,
    borderRadius: radius.xl,
  },
  buttons: { flexDirection: "row", gap: 10 },
  status: { minHeight: 24, flexDirection: "row", alignItems: "center" },
  saved: { flexDirection: "row", alignItems: "center", gap: 6 },
  row: { minHeight: 44, gap: 14, paddingVertical: 10 },
})

type Editable = "email" | "phone"

function EditableCard({
  title,
  value,
  editing,
  onEdit,
  onCancel,
  onSave,
}: {
  title: string
  value: string
  editing: boolean
  onEdit: () => void
  onCancel: () => void
  onSave: (value: string) => void
}) {
  const { colors } = useTheme()
  const [draft, setDraft] = React.useState(value)

  React.useEffect(() => {
    if (editing) setDraft(value)
  }, [editing, value])

  return (
    <View style={[styles.card, { backgroundColor: colors.muted }]}>
      <View style={styles.cardRow}>
        <View style={styles.cardText}>
          <Text role="heading" style={styles.cardTitle}>
            {title}
          </Text>
          {editing ? null : (
            <Text style={[fontSize.base, { color: colors.mutedForeground }]}>
              {value}
            </Text>
          )}
        </View>
        {editing ? null : (
          <Pressable
            role="button"
            hitSlop={8}
            onPress={onEdit}
            accessibilityLabel={`Edit ${title.toLowerCase()}`}
            style={({ pressed }) => pressed && { opacity: 0.6 }}
          >
            <Text style={[fontSize.base, { color: colors.primary }]}>
              Edit ›
            </Text>
          </Pressable>
        )}
      </View>
      {editing ? (
        <AnimatedView
          entering={FadeIn.duration(300).reduceMotion(ReduceMotion.System)}
          style={styles.editor}
        >
          <Input
            autoFocus
            value={draft}
            onChangeText={setDraft}
            accessibilityLabel={title}
            returnKeyType="done"
            onSubmitEditing={() => onSave(draft)}
            style={(state) => [
              styles.input,
              {
                backgroundColor: colors.background,
                borderColor: state.focused ? colors.ring : colors.input,
              },
            ]}
          />
          <View style={styles.buttons}>
            <Button size="sm" onPress={() => onSave(draft)}>
              <Text>Save</Text>
            </Button>
            <Button size="sm" variant="outline" onPress={onCancel}>
              <Text>Cancel</Text>
            </Button>
          </View>
        </AnimatedView>
      ) : null}
    </View>
  )
}

function Security({
  onSave,
}: {
  onSave?: (field: Editable, value: string) => void
}) {
  const { colors, isDark } = useTheme()
  const [values, setValues] = React.useState<Record<Editable, string>>({
    email: "jamie@example.com",
    phone: "+1 (415) 555-0142",
  })
  const [editing, setEditing] = React.useState<Editable | null>(null)
  const [passkeys, setPasskeys] = React.useState(true)
  const [saved, setSaved] = React.useState<string | null>(null)

  React.useEffect(() => {
    if (!saved) return
    const timer = setTimeout(() => setSaved(null), 2200)
    return () => clearTimeout(timer)
  }, [saved])

  const cards: { field: Editable; title: string }[] = [
    { field: "email", title: "Email & phone" },
    { field: "phone", title: "Trusted phone number" },
  ]

  const card = [styles.card, styles.cardRow, { backgroundColor: colors.muted }]

  return (
    <View style={{ gap: 14 }}>
      {cards.map((item) => (
        <EditableCard
          key={item.field}
          title={item.title}
          value={values[item.field]}
          editing={editing === item.field}
          onEdit={() => setEditing(item.field)}
          onCancel={() => setEditing(null)}
          onSave={(value) => {
            onSave?.(item.field, value)
            setValues((current) => ({ ...current, [item.field]: value }))
            setEditing(null)
            setSaved(item.title)
          }}
        />
      ))}
      <View style={card}>
        <View style={styles.cardText}>
          <Text nativeID="settings-account-passkeys" style={styles.cardTitle}>
            Passkeys
          </Text>
          <Text style={[fontSize.base, { color: colors.mutedForeground }]}>
            {passkeys ? "On for 3 devices" : "Off"}
          </Text>
        </View>
        <Switch
          checked={passkeys}
          onCheckedChange={setPasskeys}
          accessibilityLabelledBy="settings-account-passkeys"
        />
      </View>
      <View style={card}>
        <View style={styles.cardText}>
          <Text role="heading" style={styles.cardTitle}>
            Two-factor authentication
          </Text>
          <Text style={[fontSize.base, { color: colors.mutedForeground }]}>
            Codes are sent to your trusted devices.
          </Text>
        </View>
        <Text
          style={[
            fontSize.sm,
            {
              fontWeight: "600",
              color: isDark ? palette.green400 : palette.green600,
            },
          ]}
        >
          On
        </Text>
      </View>
      <View accessibilityLiveRegion="polite" style={styles.status}>
        {saved ? (
          <AnimatedView
            entering={FadeInDown.duration(400).reduceMotion(
              ReduceMotion.System
            )}
            style={styles.saved}
          >
            <Icon
              as={CheckIcon}
              weight="bold"
              size={16}
              color={isDark ? palette.green400 : palette.green600}
            />
            <Text style={fontSize.sm}>{saved} saved.</Text>
          </AnimatedView>
        ) : null}
      </View>
    </View>
  )
}

function SettingsAccount({
  onSave,
}: {
  onSave?: (field: "email" | "phone", value: string) => void
}) {
  const { colors } = useTheme()
  const [section, setSection] = React.useState<string | null>("security")
  const scroll = useHeaderScroll()
  const current = ITEMS.find((item) => item.value === section)

  return (
    <Screen edges={["top", "right", "left"]}>
      <Header scroll={scroll}>
        {current ? (
          <HeaderBack onPress={() => setSection(null)}>Account</HeaderBack>
        ) : null}
        <HeaderTitle>{current ? current.label : "Account"}</HeaderTitle>
      </Header>
      <HeaderScrollView key={section ?? "account"} scroll={scroll}>
        {current ? (
          <>
            <HeaderLargeTitle>{current.label}</HeaderLargeTitle>
            <AnimatedView
              entering={FadeInDown.duration(600).reduceMotion(
                ReduceMotion.System
              )}
              style={{ paddingHorizontal: 16, paddingTop: 8 }}
            >
              {current.value === "security" ? (
                <Security onSave={onSave} />
              ) : (
                <Text style={[fontSize.lg, { color: colors.mutedForeground }]}>
                  This section isn't part of the sample. Choose Sign-In &
                  Security.
                </Text>
              )}
            </AnimatedView>
          </>
        ) : (
          <>
            <HeaderLargeTitle style={{ gap: 8, paddingBottom: 16 }}>
              <Avatar size="xl" style={{ width: 72, height: 72 }}>
                <AvatarFallback style={{ backgroundColor: palette.indigo500 }}>
                  JR
                </AvatarFallback>
              </Avatar>
              <Text
                role="heading"
                style={[fontSize["2xl"], { fontWeight: "600" }]}
              >
                Jamie Rivera
              </Text>
              <Text style={[fontSize.sm, { color: colors.mutedForeground }]}>
                jamie@example.com
              </Text>
            </HeaderLargeTitle>
            <View style={{ paddingHorizontal: 16 }}>
              <ItemGroup variant="inset">
                {ITEMS.map((item, index) => (
                  <React.Fragment key={item.value}>
                    {index > 0 ? (
                      <ItemSeparator style={{ marginStart: 50 }} />
                    ) : null}
                    <Item
                      size="sm"
                      onPress={() => setSection(item.value)}
                      style={styles.row}
                    >
                      <ItemMedia>
                        <Icon as={item.icon} size={20} color={colors.primary} />
                      </ItemMedia>
                      <ItemContent>
                        <ItemTitle style={{ fontWeight: "400" }}>
                          {item.label}
                        </ItemTitle>
                      </ItemContent>
                      <ItemActions>
                        <Icon
                          as={CaretRightIcon}
                          weight="bold"
                          size={14}
                          color={colors.mutedForeground}
                          style={
                            I18nManager.isRTL
                              ? { transform: [{ rotate: "180deg" }] }
                              : undefined
                          }
                        />
                      </ItemActions>
                    </Item>
                  </React.Fragment>
                ))}
              </ItemGroup>
            </View>
          </>
        )}
        <View style={{ height: 32 }} />
      </HeaderScrollView>
    </Screen>
  )
}

export { SettingsAccount }
```

5. 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/SettingsAccount.swift`: shown above

- `swift/Components/Avatar.swift`: see [Avatar](https://ui.suiss.app/docs/components/avatar.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/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/Header.swift`: see [Header](https://ui.suiss.app/docs/components/header.md)

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

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

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

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

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

- `swift/Components/Switch.swift`: see [Switch](https://ui.suiss.app/docs/components/switch.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/SettingsAccount.kt`: shown above

- `kotlin/components/Avatar.kt`: see [Avatar](https://ui.suiss.app/docs/components/avatar.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/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/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/Header.kt`: see [Header](https://ui.suiss.app/docs/components/header.md)

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

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

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

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

- `kotlin/components/Label.kt`: see [Label](https://ui.suiss.app/docs/components/label.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/Switch.kt`: see [Switch](https://ui.suiss.app/docs/components/switch.md)

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

## Usage

```tsx
import { SettingsAccount } from "@/components/blocks/settings-account"
```

```tsx
<SettingsAccount />
```

Replace `ITEMS` and the initial email and phone values with your account data. Use `onSave`, which receives the field and new value, to persist edits.

