# Price Care

A device protection plan picker with device chips, payment options, an optional theft add-on, and a live total.

Price Care 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/price-care

**React**

**Example**

```tsx title="components/blocks/price-care.tsx"
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Switch } from "@/components/ui/switch"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

const photo = (id: string) =>
  `https://images.unsplash.com/photo-${id}?w=900&q=80&auto=format&fit=crop`

type CareOption = { label: string; price: number; theft?: number }

type Device = {
  name: string
  image: string
  alt: string
  options: [CareOption, ...CareOption[]]
}

const devices: [Device, ...Device[]] = [
  {
    name: "Phone Pro",
    image: photo("1714972384975-fbd4f19b1bb3"),
    alt: "An orange phone on a warm gradient",
    options: [
      { label: "Annual", price: 99.99, theft: 50 },
      { label: "Monthly", price: 9.99, theft: 5 },
    ],
  },
  {
    name: "Book Air",
    image: photo("1644792863360-40fa85ea52e7"),
    alt: "A laptop on a wooden sideboard",
    options: [
      { label: "3-year", price: 279 },
      { label: "Annual", price: 99.99 },
    ],
  },
  {
    name: "Watch",
    image: photo("1660844817855-3ecc7ef21f12"),
    alt: "A smartwatch with a white band",
    options: [
      { label: "2-year", price: 79 },
      { label: "Monthly", price: 3.99 },
    ],
  },
]

const money = (value: number) =>
  value.toLocaleString("en-US", { style: "currency", currency: "USD" })

export function PriceCare() {
  const [device, setDevice] = React.useState(devices[0].name)
  const [option, setOption] = React.useState(devices[0].options[0].label)
  const [theft, setTheft] = React.useState(false)
  const current = devices.find((item) => item.name === device) ?? devices[0]
  const plan =
    current.options.find((item) => item.label === option) ?? current.options[0]
  const total = plan.price + (theft ? (plan.theft ?? 0) : 0)

  return (
    <section className="grid min-h-145 bg-background md:grid-cols-[minmax(0,1fr)_minmax(0,1.1fr)]">
      <div className="relative flex min-h-80 flex-col justify-end gap-1.5 overflow-hidden bg-muted p-7">
        {devices.map((item) => (
          <img
            key={item.name}
            src={item.image}
            alt={item.alt}
            aria-hidden={item.name !== device}
            data-active={item.name === device ? "" : undefined}
            className="absolute inset-0 size-full object-cover opacity-0 transition-opacity duration-600 data-active:opacity-100 motion-reduce:transition-none"
          />
        ))}
        <span className="absolute inset-0 bg-linear-to-b from-transparent from-45% to-black/70" />
        <p className="relative text-base font-semibold text-white">
          suiss Care+
        </p>
        <p className="relative text-3xl leading-tight font-semibold tracking-tight text-white">
          Protection against accidents.
        </p>
      </div>
      <div className="flex flex-col gap-4.5 px-5 py-7.5 md:px-8">
        <h2 className="text-3xl font-semibold tracking-tight">
          Choose your device.
        </h2>
        <ToggleGroup
          value={[device]}
          onValueChange={(value: string[]) => {
            const next = devices.find((item) => item.name === value[0])
            if (!next) return
            setDevice(next.name)
            setOption(next.options[0].label)
          }}
          aria-label="Device"
          className="flex-wrap"
        >
          {devices.map((item) => (
            <ToggleGroupItem
              key={item.name}
              value={item.name}
              className="bg-secondary px-3.5 hover:bg-secondary/80 aria-pressed:bg-foreground aria-pressed:text-background aria-pressed:hover:bg-foreground aria-pressed:hover:text-background"
            >
              {item.name}
            </ToggleGroupItem>
          ))}
        </ToggleGroup>
        <RadioGroup
          key={device}
          value={option}
          onValueChange={(value) => setOption(value as string)}
          aria-label="Payment"
          className="gap-2.5"
        >
          {current.options.map((item) => (
            <Label
              key={item.label}
              className="flex cursor-pointer items-center gap-3 rounded-xl px-4 py-3.5 shadow-[inset_0_0_0_1px_var(--color-input)] transition-shadow duration-200 has-data-checked:shadow-[inset_0_0_0_2px_var(--color-primary)] has-focus-visible:ring-3 has-focus-visible:ring-ring/50 font-normal"
            >
              <RadioGroupItem
                value={item.label}
                className="size-5 focus-visible:ring-0"
              />
              <span className="flex flex-1 flex-col gap-px">
                <span className="text-base font-semibold">
                  {item.label} payment
                </span>
                <span className="text-sm text-muted-foreground">
                  Unlimited accidental damage repairs
                </span>
              </span>
              <span className="text-base font-semibold tabular-nums">
                {money(item.price)}
              </span>
            </Label>
          ))}
        </RadioGroup>
        {device === "Phone Pro" && (
          <Label className="flex cursor-pointer items-center gap-2.5 text-sm font-normal">
            <Switch checked={theft} onCheckedChange={setTheft} />
            Add theft and loss protection
          </Label>
        )}
        <div className="flex items-center gap-3 border-t border-border pt-3.5">
          <div className="flex flex-1 flex-col" aria-live="polite">
            <span className="text-sm text-muted-foreground">Total</span>
            <span className="text-2xl font-semibold tabular-nums">
              {money(total)}
            </span>
          </div>
          <Button size="lg">Add to Bag</Button>
        </div>
      </div>
    </section>
  )
}
```

A protection plan picker: choose a device, pick a payment option for it and, for some devices, add theft and loss cover, with a live total and an Add to Bag button. A photo of the selected device sits beside the options on desktop and above them on mobile. Use it for warranties, insurance add-ons or any per-product plan.

## Installation

### CLI

```bash title="Terminal"
npx shadcn@latest add @esuiss/price-care
```

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 price-care
```

### Manual

1. Add the pieces it builds on: [Button](https://ui.suiss.app/docs/components/button), [Label](https://ui.suiss.app/docs/components/label), [Radio Group](https://ui.suiss.app/docs/components/radio-group), [Switch](https://ui.suiss.app/docs/components/switch), [Toggle Group](https://ui.suiss.app/docs/components/toggle-group).

2. Copy the source into `components/blocks/price-care.tsx`:

```tsx title="components/blocks/price-care.tsx"
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Switch } from "@/components/ui/switch"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

const photo = (id: string) =>
  `https://images.unsplash.com/photo-${id}?w=900&q=80&auto=format&fit=crop`

type CareOption = { label: string; price: number; theft?: number }

type Device = {
  name: string
  image: string
  alt: string
  options: [CareOption, ...CareOption[]]
}

const devices: [Device, ...Device[]] = [
  {
    name: "Phone Pro",
    image: photo("1714972384975-fbd4f19b1bb3"),
    alt: "An orange phone on a warm gradient",
    options: [
      { label: "Annual", price: 99.99, theft: 50 },
      { label: "Monthly", price: 9.99, theft: 5 },
    ],
  },
  {
    name: "Book Air",
    image: photo("1644792863360-40fa85ea52e7"),
    alt: "A laptop on a wooden sideboard",
    options: [
      { label: "3-year", price: 279 },
      { label: "Annual", price: 99.99 },
    ],
  },
  {
    name: "Watch",
    image: photo("1660844817855-3ecc7ef21f12"),
    alt: "A smartwatch with a white band",
    options: [
      { label: "2-year", price: 79 },
      { label: "Monthly", price: 3.99 },
    ],
  },
]

const money = (value: number) =>
  value.toLocaleString("en-US", { style: "currency", currency: "USD" })

export function PriceCare() {
  const [device, setDevice] = React.useState(devices[0].name)
  const [option, setOption] = React.useState(devices[0].options[0].label)
  const [theft, setTheft] = React.useState(false)
  const current = devices.find((item) => item.name === device) ?? devices[0]
  const plan =
    current.options.find((item) => item.label === option) ?? current.options[0]
  const total = plan.price + (theft ? (plan.theft ?? 0) : 0)

  return (
    <section className="grid min-h-145 bg-background md:grid-cols-[minmax(0,1fr)_minmax(0,1.1fr)]">
      <div className="relative flex min-h-80 flex-col justify-end gap-1.5 overflow-hidden bg-muted p-7">
        {devices.map((item) => (
          <img
            key={item.name}
            src={item.image}
            alt={item.alt}
            aria-hidden={item.name !== device}
            data-active={item.name === device ? "" : undefined}
            className="absolute inset-0 size-full object-cover opacity-0 transition-opacity duration-600 data-active:opacity-100 motion-reduce:transition-none"
          />
        ))}
        <span className="absolute inset-0 bg-linear-to-b from-transparent from-45% to-black/70" />
        <p className="relative text-base font-semibold text-white">
          suiss Care+
        </p>
        <p className="relative text-3xl leading-tight font-semibold tracking-tight text-white">
          Protection against accidents.
        </p>
      </div>
      <div className="flex flex-col gap-4.5 px-5 py-7.5 md:px-8">
        <h2 className="text-3xl font-semibold tracking-tight">
          Choose your device.
        </h2>
        <ToggleGroup
          value={[device]}
          onValueChange={(value: string[]) => {
            const next = devices.find((item) => item.name === value[0])
            if (!next) return
            setDevice(next.name)
            setOption(next.options[0].label)
          }}
          aria-label="Device"
          className="flex-wrap"
        >
          {devices.map((item) => (
            <ToggleGroupItem
              key={item.name}
              value={item.name}
              className="bg-secondary px-3.5 hover:bg-secondary/80 aria-pressed:bg-foreground aria-pressed:text-background aria-pressed:hover:bg-foreground aria-pressed:hover:text-background"
            >
              {item.name}
            </ToggleGroupItem>
          ))}
        </ToggleGroup>
        <RadioGroup
          key={device}
          value={option}
          onValueChange={(value) => setOption(value as string)}
          aria-label="Payment"
          className="gap-2.5"
        >
          {current.options.map((item) => (
            <Label
              key={item.label}
              className="flex cursor-pointer items-center gap-3 rounded-xl px-4 py-3.5 shadow-[inset_0_0_0_1px_var(--color-input)] transition-shadow duration-200 has-data-checked:shadow-[inset_0_0_0_2px_var(--color-primary)] has-focus-visible:ring-3 has-focus-visible:ring-ring/50 font-normal"
            >
              <RadioGroupItem
                value={item.label}
                className="size-5 focus-visible:ring-0"
              />
              <span className="flex flex-1 flex-col gap-px">
                <span className="text-base font-semibold">
                  {item.label} payment
                </span>
                <span className="text-sm text-muted-foreground">
                  Unlimited accidental damage repairs
                </span>
              </span>
              <span className="text-base font-semibold tabular-nums">
                {money(item.price)}
              </span>
            </Label>
          ))}
        </RadioGroup>
        {device === "Phone Pro" && (
          <Label className="flex cursor-pointer items-center gap-2.5 text-sm font-normal">
            <Switch checked={theft} onCheckedChange={setTheft} />
            Add theft and loss protection
          </Label>
        )}
        <div className="flex items-center gap-3 border-t border-border pt-3.5">
          <div className="flex flex-1 flex-col" aria-live="polite">
            <span className="text-sm text-muted-foreground">Total</span>
            <span className="text-2xl font-semibold tabular-nums">
              {money(total)}
            </span>
          </div>
          <Button size="lg">Add to Bag</Button>
        </div>
      </div>
    </section>
  )
}
```

3. Update the import paths to match your project.

## Usage

```tsx
import { PriceCare } from "@/components/blocks/price-care"
```

```tsx
<PriceCare />
```

Edit `devices`; each has a name, photo, alt text and payment options with a label, price and optional theft surcharge. The theft switch only appears for Phone Pro, so change that check if other devices offer it. Give Add to Bag a handler.

## Notes

The total is announced when it changes. Photos are Unsplash placeholders.

**React Native, SwiftUI, Jetpack Compose**

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

```tsx title="uniwind/components/blocks/price-care.tsx"
import { cn } from "cn"
import * as React from "react"
import { Image, Pressable, ScrollView, View } from "react-native"
import {
  FadeInDown,
  ReduceMotion,
  useAnimatedStyle,
  useSharedValue,
  withTiming,
} from "react-native-reanimated"
import { AnimatedView } from "@/components/ui/animated-view"
import { BottomBar, BottomBarContent } from "@/components/ui/bottom-bar"
import { Button } from "@/components/ui/button"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Screen } from "@/components/ui/screen"
import { Switch } from "@/components/ui/switch"
import { Text } from "@/components/ui/text"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

const photo = (id: string) =>
  `https://images.unsplash.com/photo-${id}?w=900&q=80&auto=format&fit=crop`

type CareOption = { label: string; price: number; theft?: number }

type Device = {
  name: string
  image: string
  alt: string
  options: [CareOption, ...CareOption[]]
}

const DEVICES: [Device, ...Device[]] = [
  {
    name: "Phone Pro",
    image: photo("1714972384975-fbd4f19b1bb3"),
    alt: "An orange phone on a warm gradient",
    options: [
      { label: "Annual", price: 99.99, theft: 50 },
      { label: "Monthly", price: 9.99, theft: 5 },
    ],
  },
  {
    name: "Book Air",
    image: photo("1644792863360-40fa85ea52e7"),
    alt: "A laptop on a wooden sideboard",
    options: [
      { label: "3-year", price: 279 },
      { label: "Annual", price: 99.99 },
    ],
  },
  {
    name: "Watch",
    image: photo("1660844817855-3ecc7ef21f12"),
    alt: "A smartwatch with a white band",
    options: [
      { label: "2-year", price: 79 },
      { label: "Monthly", price: 3.99 },
    ],
  },
]

const money = (value: number) => {
  try {
    return new Intl.NumberFormat("en-US", {
      style: "currency",
      currency: "USD",
    }).format(value)
  } catch {
    const [whole, fraction] = value.toFixed(2).split(".")
    const grouped = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ",")
    return `$${grouped}.${fraction}`
  }
}

function HeroImage({ device, active }: { device: Device; active: boolean }) {
  const opacity = useSharedValue(active ? 1 : 0)

  React.useEffect(() => {
    opacity.value = withTiming(active ? 1 : 0, {
      duration: 600,
      reduceMotion: ReduceMotion.System,
    })
  }, [active, opacity])

  const animatedStyle = useAnimatedStyle(() => ({ opacity: opacity.value }))

  return (
    <AnimatedView
      accessible={active}
      accessibilityRole="image"
      accessibilityLabel={device.alt}
      importantForAccessibility={active ? "yes" : "no-hide-descendants"}
      accessibilityElementsHidden={!active}
      className="absolute inset-0"
      style={animatedStyle}
    >
      <Image
        source={{ uri: device.image }}
        resizeMode="cover"
        accessibilityIgnoresInvertColors
        className="size-full"
      />
    </AnimatedView>
  )
}

function PriceCare({
  onAddToBag,
}: {
  onAddToBag?: (selection: {
    device: string
    option: string
    theft: boolean
    total: number
  }) => void
}) {
  const [device, setDevice] = React.useState(DEVICES[0].name)
  const [option, setOption] = React.useState(DEVICES[0].options[0].label)
  const [theft, setTheft] = React.useState(false)
  const current = DEVICES.find((item) => item.name === device) ?? DEVICES[0]
  const plan =
    current.options.find((item) => item.label === option) ?? current.options[0]
  const withTheft = theft && current.name === "Phone Pro"
  const total = plan.price + (withTheft ? (plan.theft ?? 0) : 0)

  return (
    <Screen edges={["top", "right", "left"]}>
      <ScrollView contentContainerClassName="pb-6">
        <View className="min-h-80 justify-end overflow-hidden bg-black">
          {DEVICES.map((item) => (
            <HeroImage
              key={item.name}
              device={item}
              active={item.name === device}
            />
          ))}
          <View className="gap-1.5 bg-black/50 px-7 pt-5 pb-7">
            <Text className="text-base font-semibold text-white">
              suiss Care+
            </Text>
            <Text role="heading" className="text-3xl font-semibold text-white">
              Protection against accidents.
            </Text>
          </View>
        </View>
        <AnimatedView
          entering={FadeInDown.duration(600).reduceMotion(ReduceMotion.System)}
          className="gap-4.5 px-5 pt-7.5"
        >
          <Text role="heading" className="text-3xl font-semibold">
            Choose your device.
          </Text>
          <ToggleGroup
            value={[device]}
            onValueChange={(value) => {
              const next = DEVICES.find((item) => item.name === value[0])
              if (!next) return
              setDevice(next.name)
              setOption(next.options[0].label)
            }}
            accessibilityLabel="Device"
            className="flex-wrap"
          >
            {DEVICES.map((item) => {
              const selected = item.name === device
              return (
                <ToggleGroupItem
                  key={item.name}
                  value={item.name}
                  className={cn(
                    "px-3.5",
                    selected
                      ? "bg-foreground active:opacity-80"
                      : "bg-secondary active:opacity-80"
                  )}
                >
                  <Text
                    className={selected ? "text-background" : "text-foreground"}
                  >
                    {item.name}
                  </Text>
                </ToggleGroupItem>
              )
            })}
          </ToggleGroup>
          <RadioGroup
            key={device}
            value={option}
            onValueChange={setOption}
            accessibilityLabel="Payment"
            className="gap-2.5"
          >
            {current.options.map((item) => {
              const checked = item.label === option
              return (
                <Pressable
                  key={item.label}
                  onPress={() => setOption(item.label)}
                  className={cn(
                    "flex-row items-center gap-3 rounded-xl border-2 px-4 py-3.5",
                    checked
                      ? "border-primary"
                      : "border-border active:opacity-80"
                  )}
                >
                  <RadioGroupItem
                    value={item.label}
                    accessibilityLabel={`${item.label} payment, ${money(item.price)}`}
                    className="size-5"
                  />
                  <View className="flex-1 gap-px">
                    <Text className="text-base font-semibold">
                      {item.label} payment
                    </Text>
                    <Text className="text-sm text-muted-foreground">
                      Unlimited accidental damage repairs
                    </Text>
                  </View>
                  <Text className="text-base font-semibold tabular-nums">
                    {money(item.price)}
                  </Text>
                </Pressable>
              )
            })}
          </RadioGroup>
          {current.name === "Phone Pro" ? (
            <Pressable
              accessible={false}
              onPress={() => setTheft((value) => !value)}
              className="flex-row items-center gap-2.5"
            >
              <Switch
                checked={theft}
                onCheckedChange={setTheft}
                accessibilityLabelledBy="price-care-theft"
              />
              <Text nativeID="price-care-theft" className="flex-1 text-sm">
                Add theft and loss protection
              </Text>
            </Pressable>
          ) : null}
        </AnimatedView>
      </ScrollView>
      <BottomBar separator>
        <BottomBarContent className="flex-row items-center gap-3">
          <View accessibilityLiveRegion="polite" className="flex-1">
            <Text className="text-sm text-muted-foreground">Total</Text>
            <Text className="text-2xl font-semibold tabular-nums">
              {money(total)}
            </Text>
          </View>
          <Button
            size="lg"
            onPress={() =>
              onAddToBag?.({
                device: current.name,
                option: plan.label,
                theft: withTheft,
                total,
              })
            }
          >
            <Text>Add to Bag</Text>
          </Button>
        </BottomBarContent>
      </BottomBar>
    </Screen>
  )
}

export { PriceCare }
```

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

public struct PriceCare: View {
  struct Option: Identifiable {
    let label: String
    let price: Double
    var theft: Double?

    var id: String { label }
  }

  struct Device: Identifiable {
    let name: String
    let image: URL?
    let alt: String
    let options: [Option]

    var id: String { name }
  }

  static func photo(_ id: String) -> URL? {
    URL(string: "https://images.unsplash.com/photo-\(id)?w=900&q=80&auto=format&fit=crop")
  }

  static var devices: [Device] {
    [
      Device(name: "Phone Pro", image: photo("1714972384975-fbd4f19b1bb3"), alt: "An orange phone on a warm gradient", options: [
        Option(label: "Annual", price: 99.99, theft: 50),
        Option(label: "Monthly", price: 9.99, theft: 5),
      ]),
      Device(name: "Book Air", image: photo("1644792863360-40fa85ea52e7"), alt: "A laptop on a wooden sideboard", options: [
        Option(label: "3-year", price: 279),
        Option(label: "Annual", price: 99.99),
      ]),
      Device(name: "Watch", image: photo("1660844817855-3ecc7ef21f12"), alt: "A smartwatch with a white band", options: [
        Option(label: "2-year", price: 79),
        Option(label: "Monthly", price: 3.99),
      ]),
    ]
  }

  private var onAddToBag: ((String, String, Bool, Double) -> Void)?
  @State private var device: String
  @State private var option: String
  @State private var theft: Bool
  @State private var added = false
  @Environment(\.locale) private var locale
  @Environment(\.accessibilityReduceMotion) private var reduceMotion

  public init(onAddToBag: ((_ device: String, _ option: String, _ theft: Bool, _ total: Double) -> Void)? = nil) {
    self.init(device: "Phone Pro")
    self.onAddToBag = onAddToBag
  }

  init(device: String, option: String? = nil, theft: Bool = false) {
    let current = Self.devices.first { $0.name == device } ?? Self.devices[0]
    _device = State(initialValue: current.name)
    _option = State(initialValue: option ?? current.options[0].label)
    _theft = State(initialValue: theft)
  }

  private var current: Device {
    Self.devices.first { $0.name == device } ?? Self.devices[0]
  }

  private var plan: Option {
    current.options.first { $0.label == option } ?? current.options[0]
  }

  private var total: Double {
    plan.price + (theft ? plan.theft ?? 0 : 0)
  }

  private func money(_ value: Double) -> String {
    value.formatted(.currency(code: "USD").locale(locale))
  }

  public var body: some View {
    Screen {
      KeyboardScrollView {
        VStack(alignment: .leading, spacing: 0) {
          hero
          VStack(alignment: .leading, spacing: 18) {
            Text("Choose your device.")
              .scaledTextStyle(.xl2, weight: .semibold)
              .accessibilityAddTraits(.isHeader)
            ToggleGroup(selection: deviceSelection, spacing: 0) {
              ForEach(Self.devices) { item in
                ToggleGroupItem(value: item.name) {
                  Text(LocalizedStringKey(item.name))
                }
              }
            }
            .accessibilityLabel(Text("Device"))
            RadioGroup(selection: $option) {
              ForEach(current.options) { item in
                PriceCareChoice(value: item.label, selection: $option) {
                  VStack(alignment: .leading, spacing: 1) {
                    Text("\(Text(LocalizedStringKey(item.label))) payment")
                      .scaledFont(.base, weight: .semibold)
                    Text("Unlimited accidental damage repairs")
                      .scaledFont(.sm)
                      .foregroundStyle(EsuissColor.mutedForeground)
                  }
                  .frame(maxWidth: .infinity, alignment: .leading)
                  Text(verbatim: money(item.price))
                    .scaledFont(.base, weight: .semibold)
                    .monospacedDigit()
                }
              }
            }
            .id(device)
            .accessibilityLabel(Text("Payment"))
            if current.options.contains(where: { $0.theft != nil }) {
              Toggle(isOn: $theft) {
                Text("Add theft and loss protection")
                  .scaledFont(.sm)
                  .foregroundStyle(EsuissColor.foreground)
              }
              .toggleStyle(SwitchStyle())
            }
          }
          .padding(.horizontal, 20)
          .padding(.vertical, 30)
        }
      }
      BottomBar {
        BottomBarContent {
          HStack(spacing: 12) {
            VStack(alignment: .leading, spacing: 0) {
              Text("Total")
                .scaledFont(.sm)
                .foregroundStyle(EsuissColor.mutedForeground)
              Text(verbatim: money(total))
                .scaledFont(.xl, weight: .semibold)
                .monospacedDigit()
                .contentTransition(.numericText())
            }
            .frame(maxWidth: .infinity, alignment: .leading)
            .accessibilityElement(children: .combine)
            Button(added ? "Added to Bag" : "Add to Bag") {
              onAddToBag?(device, option, theft, total)
              added = true
            }
            .buttonStyle(.variant(.default, size: .lg))
          }
        }
      }
    }
    .animation(reduceMotion ? nil : .easeOut(duration: 0.25), value: total)
    .animation(reduceMotion ? nil : .easeOut(duration: 0.25), value: device)
    .onChange(of: total) { added = false }
  }

  private var deviceSelection: Binding<String?> {
    Binding(
      get: { device },
      set: { value in
        guard let value, let next = Self.devices.first(where: { $0.name == value }) else { return }
        device = next.name
        option = next.options[0].label
        theft = false
        added = false
      }
    )
  }

  private var hero: some View {
    VStack(alignment: .leading, spacing: 6) {
      Text(verbatim: "suiss Care+")
        .scaledFont(.base, weight: .semibold)
      Text("Protection against accidents.")
        .scaledTextStyle(.xl2, weight: .semibold)
    }
    .foregroundStyle(Palette.white)
    .fixedSize(horizontal: false, vertical: true)
    .padding(28)
    .frame(maxWidth: .infinity, minHeight: 320, alignment: .bottomLeading)
    .background {
      Palette.black.overlay {
        ForEach(Self.devices) { item in
          Color.clear.overlay {
            AsyncImage(url: item.image) { phase in
              if let image = phase.image {
                image.resizable().scaledToFill()
              }
            }
          }
          .opacity(item.name == device ? 1 : 0)
          .animation(reduceMotion ? nil : .easeOut(duration: 0.6), value: device)
          .accessibilityHidden(item.name != device)
          .accessibilityLabel(Text(LocalizedStringKey(item.alt)))
        }
      }
      .overlay {
        LinearGradient(
          stops: [.init(color: .clear, location: 0.45), .init(color: Palette.black.opacity(0.7), location: 1)],
          startPoint: .top,
          endPoint: .bottom
        )
      }
      .clipped()
    }
    .accessibilityElement(children: .combine)
  }
}

private struct PriceCareChoice<Content: View>: View {
  let value: String
  @Binding var selection: String
  let content: Content

  init(value: String, selection: Binding<String>, @ViewBuilder content: () -> Content) {
    self.value = value
    _selection = selection
    self.content = content()
  }

  var body: some View {
    let shape = RoundedRectangle(cornerRadius: Radius.xl, style: .continuous)
    let checked = value == selection
    RadioGroupItem(value: value) {
      HStack(spacing: 12) {
        content
      }
      .foregroundStyle(EsuissColor.foreground)
      .frame(maxWidth: .infinity, alignment: .leading)
    }
    .padding(.horizontal, 16)
    .padding(.vertical, 14)
    .overlay {
      shape.strokeBorder(checked ? EsuissColor.primary : EsuissColor.input, lineWidth: checked ? 2 : 1)
    }
    .contentShape(shape)
    .onTapGesture { selection = value }
  }
}
```

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

import androidx.compose.animation.Crossfade
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.toggleable
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.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.semantics.LiveRegionMode
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription
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.Light
import com.adamglin.phosphoricons.light.DeviceMobile
import com.adamglin.phosphoricons.light.Laptop
import com.adamglin.phosphoricons.light.Watch
import com.esuiss.ui.components.BottomBar
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonSize
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.RadioGroup
import com.esuiss.ui.components.RadioGroupItem
import com.esuiss.ui.components.Screen
import com.esuiss.ui.components.Switch
import com.esuiss.ui.components.ToggleGroup
import com.esuiss.ui.components.ToggleGroupItem
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Palette
import com.esuiss.ui.theme.Typography
import java.text.NumberFormat
import java.util.Currency
import java.util.Locale

private class CareOption(val label: String, val price: Double, val theft: Double? = null)

private class CareDevice(val name: String, val icon: ImageVector, val alt: String, val options: List<CareOption>)

private val CareDevices = listOf(
    CareDevice(
        "Phone Pro",
        PhosphorIcons.Light.DeviceMobile,
        "An orange phone on a warm gradient",
        listOf(CareOption("Annual", 99.99, 50.0), CareOption("Monthly", 9.99, 5.0)),
    ),
    CareDevice(
        "Book Air",
        PhosphorIcons.Light.Laptop,
        "A laptop on a wooden sideboard",
        listOf(CareOption("3-year", 279.0), CareOption("Annual", 99.99)),
    ),
    CareDevice(
        "Watch",
        PhosphorIcons.Light.Watch,
        "A smartwatch with a white band",
        listOf(CareOption("2-year", 79.0), CareOption("Monthly", 3.99)),
    ),
)

private fun careMoney(value: Double): String =
    NumberFormat.getCurrencyInstance(Locale.US).apply { currency = Currency.getInstance("USD") }.format(value)

@Composable
fun PriceCare(
    modifier: Modifier = Modifier,
    onAddToBag: (device: String, option: String, theft: Boolean, total: Double) -> Unit = { _, _, _, _ -> },
) {
    val colors = EsuissTheme.colors
    var device by rememberSaveable { mutableStateOf(CareDevices.first().name) }
    var option by rememberSaveable { mutableStateOf(CareDevices.first().options.first().label) }
    var theft by rememberSaveable { mutableStateOf(false) }
    val current = CareDevices.firstOrNull { it.name == device } ?: CareDevices.first()
    val plan = current.options.firstOrNull { it.label == option } ?: current.options.first()
    val withTheft = theft && device == "Phone Pro"
    val total = plan.price + if (withTheft) plan.theft ?: 0.0 else 0.0

    Screen(modifier) {
        Column(Modifier.fillMaxSize()) {
            Column(
                Modifier
                    .weight(1f)
                    .verticalScroll(rememberScrollState()),
            ) {
                CareHero(current)
                Column(
                    Modifier.padding(horizontal = 20.dp, vertical = 30.dp),
                    verticalArrangement = Arrangement.spacedBy(18.dp),
                ) {
                    BasicText(
                        "Choose your device.",
                        modifier = Modifier.semantics { heading() },
                        style = Typography.xl2.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold),
                    )
                    ToggleGroup(
                        value = device,
                        onValueChange = { value ->
                            val next = CareDevices.firstOrNull { it.name == value } ?: return@ToggleGroup
                            device = next.name
                            option = next.options.first().label
                        },
                        modifier = Modifier.semantics { contentDescription = "Device" },
                    ) {
                        CareDevices.forEach { item -> ToggleGroupItem(item.name, item.name) }
                    }
                    RadioGroup(
                        value = plan.label,
                        onValueChange = { option = it },
                        modifier = Modifier.semantics { contentDescription = "Payment" },
                    ) {
                        current.options.forEach { item ->
                            CareChoice(item.label, item.label == plan.label) {
                                Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(1.dp)) {
                                    BasicText(
                                        "${item.label} payment",
                                        style = Typography.base.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold),
                                    )
                                    BasicText(
                                        "Unlimited accidental damage repairs",
                                        style = Typography.sm.copy(color = colors.mutedForeground),
                                    )
                                }
                                BasicText(
                                    careMoney(item.price),
                                    style = Typography.base.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold, fontFeatureSettings = "tnum"),
                                )
                            }
                        }
                    }
                    if (device == "Phone Pro") {
                        Row(
                            modifier = Modifier.toggleable(
                                value = theft,
                                interactionSource = remember { MutableInteractionSource() },
                                indication = null,
                                role = Role.Switch,
                            ) { theft = it },
                            horizontalArrangement = Arrangement.spacedBy(10.dp),
                            verticalAlignment = Alignment.CenterVertically,
                        ) {
                            Switch(checked = theft, onCheckedChange = null)
                            BasicText("Add theft and loss protection", style = Typography.sm.copy(color = colors.foreground))
                        }
                    }
                }
            }
            BottomBar(separator = true) {
                Row(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(horizontal = 16.dp, vertical = 12.dp),
                    horizontalArrangement = Arrangement.spacedBy(12.dp),
                    verticalAlignment = Alignment.CenterVertically,
                ) {
                    Column(
                        Modifier
                            .weight(1f)
                            .semantics(mergeDescendants = true) { liveRegion = LiveRegionMode.Polite },
                    ) {
                        BasicText("Total", style = Typography.sm.copy(color = colors.mutedForeground))
                        BasicText(
                            careMoney(total),
                            style = Typography.xl.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold, fontFeatureSettings = "tnum"),
                        )
                    }
                    Button("Add to Bag", { onAddToBag(device, plan.label, withTheft, total) }, size = ButtonSize.Lg)
                }
            }
        }
    }
}

@Composable
private fun CareHero(device: CareDevice) {
    val colors = EsuissTheme.colors
    Box(
        Modifier
            .fillMaxWidth()
            .height(320.dp)
            .background(colors.muted),
    ) {
        Crossfade(device, animationSpec = tween(600), label = "device") { shown ->
            Box(
                Modifier
                    .fillMaxSize()
                    .padding(bottom = 80.dp)
                    .clearAndSetSemantics { contentDescription = shown.alt },
                contentAlignment = Alignment.Center,
            ) {
                Icon(shown.icon, contentDescription = null, size = 150.dp, tint = colors.mutedForeground)
            }
        }
        Box(
            Modifier
                .fillMaxSize()
                .background(Brush.verticalGradient(0.45f to Color.Transparent, 1f to Palette.black.copy(alpha = 0.7f))),
        )
        Column(
            Modifier
                .align(Alignment.BottomStart)
                .padding(28.dp),
            verticalArrangement = Arrangement.spacedBy(6.dp),
        ) {
            BasicText("suiss Care+", style = Typography.base.copy(color = Palette.white, fontWeight = FontWeight.SemiBold))
            BasicText(
                "Protection against accidents.",
                modifier = Modifier.semantics { heading() },
                style = Typography.xl3.copy(color = Palette.white, fontWeight = FontWeight.SemiBold),
            )
        }
    }
}

@Composable
private fun CareChoice(value: String, selected: Boolean, content: @Composable RowScope.() -> Unit) {
    val colors = EsuissTheme.colors
    val shape = RoundedCornerShape(12.dp)
    val border by animateColorAsState(if (selected) colors.primary else colors.input, tween(200), label = "border")
    RadioGroupItem(
        value = value,
        modifier = Modifier
            .fillMaxWidth()
            .border(if (selected) 2.dp else 1.dp, border, shape)
            .padding(horizontal = 16.dp, vertical = 14.dp),
        label = {
            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(start = 4.dp),
                horizontalArrangement = Arrangement.spacedBy(12.dp),
                verticalAlignment = Alignment.CenterVertically,
                content = content,
            )
        },
    )
}
```

A product screen with a hero photo per device, a `ToggleGroup` to pick the device, and a `RadioGroup` of payment options. For the phone, a `Switch` adds theft and loss protection. A `BottomBar` shows the total, which updates with each choice, and an Add to Bag `Button`.

## Installation

**React Native**

```bash title="Terminal"
npx shadcn@latest add @esuiss-rn/price-care
```

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 price-care
```

**SwiftUI**

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

**Jetpack Compose**

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

The CLI also adds what it builds on: [Bottom Bar](https://ui.suiss.app/docs/components/bottom-bar), [Button](https://ui.suiss.app/docs/components/button), [Radio Group](https://ui.suiss.app/docs/components/radio-group), [Screen](https://ui.suiss.app/docs/components/screen), [Switch](https://ui.suiss.app/docs/components/switch), [Text](https://ui.suiss.app/docs/components/text), [Toggle Group](https://ui.suiss.app/docs/components/toggle-group). 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 react-native-reanimated react-native-worklets
```

2. Add the pieces it builds on: [Bottom Bar](https://ui.suiss.app/docs/components/bottom-bar), [Button](https://ui.suiss.app/docs/components/button), [Radio Group](https://ui.suiss.app/docs/components/radio-group), [Screen](https://ui.suiss.app/docs/components/screen), [Switch](https://ui.suiss.app/docs/components/switch), [Text](https://ui.suiss.app/docs/components/text), [Toggle Group](https://ui.suiss.app/docs/components/toggle-group), 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/price-care.tsx`:

- `uniwind/components/blocks/price-care.tsx`: shown above

5. Update the import paths to match your project.

**NativeWind**

1. Install the dependencies:

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

2. Add the pieces it builds on: [Bottom Bar](https://ui.suiss.app/docs/components/bottom-bar), [Button](https://ui.suiss.app/docs/components/button), [Radio Group](https://ui.suiss.app/docs/components/radio-group), [Screen](https://ui.suiss.app/docs/components/screen), [Switch](https://ui.suiss.app/docs/components/switch), [Text](https://ui.suiss.app/docs/components/text), [Toggle Group](https://ui.suiss.app/docs/components/toggle-group), 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/price-care.tsx`:

```tsx title="nativewind/components/blocks/price-care.tsx"
import { cn } from "cn"
import * as React from "react"
import { Image, Pressable, ScrollView, View } from "react-native"
import {
  FadeInDown,
  ReduceMotion,
  useAnimatedStyle,
  useSharedValue,
  withTiming,
} from "react-native-reanimated"
import { AnimatedView } from "@/components/ui/animated-view"
import { BottomBar, BottomBarContent } from "@/components/ui/bottom-bar"
import { Button } from "@/components/ui/button"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Screen } from "@/components/ui/screen"
import { Switch } from "@/components/ui/switch"
import { Text } from "@/components/ui/text"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

const photo = (id: string) =>
  `https://images.unsplash.com/photo-${id}?w=900&q=80&auto=format&fit=crop`

type CareOption = { label: string; price: number; theft?: number }

type Device = {
  name: string
  image: string
  alt: string
  options: [CareOption, ...CareOption[]]
}

const DEVICES: [Device, ...Device[]] = [
  {
    name: "Phone Pro",
    image: photo("1714972384975-fbd4f19b1bb3"),
    alt: "An orange phone on a warm gradient",
    options: [
      { label: "Annual", price: 99.99, theft: 50 },
      { label: "Monthly", price: 9.99, theft: 5 },
    ],
  },
  {
    name: "Book Air",
    image: photo("1644792863360-40fa85ea52e7"),
    alt: "A laptop on a wooden sideboard",
    options: [
      { label: "3-year", price: 279 },
      { label: "Annual", price: 99.99 },
    ],
  },
  {
    name: "Watch",
    image: photo("1660844817855-3ecc7ef21f12"),
    alt: "A smartwatch with a white band",
    options: [
      { label: "2-year", price: 79 },
      { label: "Monthly", price: 3.99 },
    ],
  },
]

const money = (value: number) => {
  try {
    return new Intl.NumberFormat("en-US", {
      style: "currency",
      currency: "USD",
    }).format(value)
  } catch {
    const [whole, fraction] = value.toFixed(2).split(".")
    const grouped = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ",")
    return `$${grouped}.${fraction}`
  }
}

function HeroImage({ device, active }: { device: Device; active: boolean }) {
  const opacity = useSharedValue(active ? 1 : 0)

  React.useEffect(() => {
    opacity.value = withTiming(active ? 1 : 0, {
      duration: 600,
      reduceMotion: ReduceMotion.System,
    })
  }, [active, opacity])

  const animatedStyle = useAnimatedStyle(() => ({ opacity: opacity.value }))

  return (
    <AnimatedView
      accessible={active}
      accessibilityRole="image"
      accessibilityLabel={device.alt}
      importantForAccessibility={active ? "yes" : "no-hide-descendants"}
      accessibilityElementsHidden={!active}
      className="absolute inset-0"
      style={animatedStyle}
    >
      <Image
        source={{ uri: device.image }}
        resizeMode="cover"
        accessibilityIgnoresInvertColors
        className="size-full"
      />
    </AnimatedView>
  )
}

function PriceCare({
  onAddToBag,
}: {
  onAddToBag?: (selection: {
    device: string
    option: string
    theft: boolean
    total: number
  }) => void
}) {
  const [device, setDevice] = React.useState(DEVICES[0].name)
  const [option, setOption] = React.useState(DEVICES[0].options[0].label)
  const [theft, setTheft] = React.useState(false)
  const current = DEVICES.find((item) => item.name === device) ?? DEVICES[0]
  const plan =
    current.options.find((item) => item.label === option) ?? current.options[0]
  const withTheft = theft && current.name === "Phone Pro"
  const total = plan.price + (withTheft ? (plan.theft ?? 0) : 0)

  return (
    <Screen edges={["top", "right", "left"]}>
      <ScrollView contentContainerClassName="pb-6">
        <View className="min-h-80 justify-end overflow-hidden bg-black">
          {DEVICES.map((item) => (
            <HeroImage
              key={item.name}
              device={item}
              active={item.name === device}
            />
          ))}
          <View className="gap-1.5 bg-black/50 px-7 pt-5 pb-7">
            <Text className="text-base font-semibold text-white">
              suiss Care+
            </Text>
            <Text role="heading" className="text-3xl font-semibold text-white">
              Protection against accidents.
            </Text>
          </View>
        </View>
        <AnimatedView
          entering={FadeInDown.duration(600).reduceMotion(ReduceMotion.System)}
          className="gap-[1.125rem] px-5 pt-[1.875rem]"
        >
          <Text role="heading" className="text-3xl font-semibold">
            Choose your device.
          </Text>
          <ToggleGroup
            value={[device]}
            onValueChange={(value) => {
              const next = DEVICES.find((item) => item.name === value[0])
              if (!next) return
              setDevice(next.name)
              setOption(next.options[0].label)
            }}
            accessibilityLabel="Device"
            className="flex-wrap"
          >
            {DEVICES.map((item) => {
              const selected = item.name === device
              return (
                <ToggleGroupItem
                  key={item.name}
                  value={item.name}
                  className={cn(
                    "px-3.5",
                    selected
                      ? "bg-foreground active:opacity-80"
                      : "bg-secondary active:opacity-80"
                  )}
                >
                  <Text
                    className={selected ? "text-background" : "text-foreground"}
                  >
                    {item.name}
                  </Text>
                </ToggleGroupItem>
              )
            })}
          </ToggleGroup>
          <RadioGroup
            key={device}
            value={option}
            onValueChange={setOption}
            accessibilityLabel="Payment"
            className="gap-2.5"
          >
            {current.options.map((item) => {
              const checked = item.label === option
              return (
                <Pressable
                  key={item.label}
                  onPress={() => setOption(item.label)}
                  className={cn(
                    "flex-row items-center gap-3 rounded-xl border-2 px-4 py-3.5",
                    checked
                      ? "border-primary"
                      : "border-border active:opacity-80"
                  )}
                >
                  <RadioGroupItem
                    value={item.label}
                    accessibilityLabel={`${item.label} payment, ${money(item.price)}`}
                    className="size-5"
                  />
                  <View className="flex-1 gap-px">
                    <Text className="text-base font-semibold">
                      {item.label} payment
                    </Text>
                    <Text className="text-sm text-muted-foreground">
                      Unlimited accidental damage repairs
                    </Text>
                  </View>
                  <Text className="text-base font-semibold tabular-nums">
                    {money(item.price)}
                  </Text>
                </Pressable>
              )
            })}
          </RadioGroup>
          {current.name === "Phone Pro" ? (
            <Pressable
              accessible={false}
              onPress={() => setTheft((value) => !value)}
              className="flex-row items-center gap-2.5"
            >
              <Switch
                checked={theft}
                onCheckedChange={setTheft}
                accessibilityLabelledBy="price-care-theft"
              />
              <Text nativeID="price-care-theft" className="flex-1 text-sm">
                Add theft and loss protection
              </Text>
            </Pressable>
          ) : null}
        </AnimatedView>
      </ScrollView>
      <BottomBar separator>
        <BottomBarContent className="flex-row items-center gap-3">
          <View accessibilityLiveRegion="polite" className="flex-1">
            <Text className="text-sm text-muted-foreground">Total</Text>
            <Text className="text-2xl font-semibold tabular-nums">
              {money(total)}
            </Text>
          </View>
          <Button
            size="lg"
            onPress={() =>
              onAddToBag?.({
                device: current.name,
                option: plan.label,
                theft: withTheft,
                total,
              })
            }
          >
            <Text>Add to Bag</Text>
          </Button>
        </BottomBarContent>
      </BottomBar>
    </Screen>
  )
}

export { PriceCare }
```

5. Update the import paths to match your project.

**StyleSheet**

1. Install the dependencies:

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

2. Add the pieces it builds on: [Bottom Bar](https://ui.suiss.app/docs/components/bottom-bar), [Button](https://ui.suiss.app/docs/components/button), [Radio Group](https://ui.suiss.app/docs/components/radio-group), [Screen](https://ui.suiss.app/docs/components/screen), [Switch](https://ui.suiss.app/docs/components/switch), [Text](https://ui.suiss.app/docs/components/text), [Toggle Group](https://ui.suiss.app/docs/components/toggle-group), 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/price-care.tsx`:

```tsx title="stylesheet/components/blocks/price-care.tsx"
import * as React from "react"
import { Image, Pressable, ScrollView, StyleSheet, View } from "react-native"
import {
  FadeInDown,
  ReduceMotion,
  useAnimatedStyle,
  useSharedValue,
  withTiming,
} from "react-native-reanimated"
import { AnimatedView } from "@/components/ui/animated-view"
import { BottomBar, BottomBarContent } from "@/components/ui/bottom-bar"
import { Button } from "@/components/ui/button"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
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"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

const photo = (id: string) =>
  `https://images.unsplash.com/photo-${id}?w=900&q=80&auto=format&fit=crop`

type CareOption = { label: string; price: number; theft?: number }

type Device = {
  name: string
  image: string
  alt: string
  options: [CareOption, ...CareOption[]]
}

const DEVICES: [Device, ...Device[]] = [
  {
    name: "Phone Pro",
    image: photo("1714972384975-fbd4f19b1bb3"),
    alt: "An orange phone on a warm gradient",
    options: [
      { label: "Annual", price: 99.99, theft: 50 },
      { label: "Monthly", price: 9.99, theft: 5 },
    ],
  },
  {
    name: "Book Air",
    image: photo("1644792863360-40fa85ea52e7"),
    alt: "A laptop on a wooden sideboard",
    options: [
      { label: "3-year", price: 279 },
      { label: "Annual", price: 99.99 },
    ],
  },
  {
    name: "Watch",
    image: photo("1660844817855-3ecc7ef21f12"),
    alt: "A smartwatch with a white band",
    options: [
      { label: "2-year", price: 79 },
      { label: "Monthly", price: 3.99 },
    ],
  },
]

const money = (value: number) => {
  try {
    return new Intl.NumberFormat("en-US", {
      style: "currency",
      currency: "USD",
    }).format(value)
  } catch {
    const [whole, fraction] = value.toFixed(2).split(".")
    const grouped = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ",")
    return `$${grouped}.${fraction}`
  }
}

const styles = StyleSheet.create({
  content: { paddingBottom: 24 },
  hero: { minHeight: 320, justifyContent: "flex-end", overflow: "hidden" },
  layer: { position: "absolute", top: 0, right: 0, bottom: 0, left: 0 },
  image: { width: "100%", height: "100%" },
  band: {
    gap: 6,
    paddingHorizontal: 28,
    paddingTop: 20,
    paddingBottom: 28,
  },
  kicker: { ...fontSize.base, fontWeight: "600" },
  heading: { ...fontSize["3xl"], fontWeight: "600" },
  body: { gap: 18, paddingHorizontal: 20, paddingTop: 30 },
  devices: { flexWrap: "wrap" },
  device: { paddingHorizontal: 14 },
  options: { gap: 10 },
  option: {
    flexDirection: "row",
    alignItems: "center",
    gap: 12,
    borderRadius: radius.xl,
    borderWidth: 2,
    paddingHorizontal: 16,
    paddingVertical: 14,
  },
  radio: { width: 20, height: 20 },
  optionText: { flex: 1, gap: 1 },
  strong: { ...fontSize.base, fontWeight: "600" },
  price: {
    ...fontSize.base,
    fontWeight: "600",
    fontVariant: ["tabular-nums"],
  },
  theft: { flexDirection: "row", alignItems: "center", gap: 10 },
  bar: { flexDirection: "row", alignItems: "center", gap: 12 },
  total: {
    ...fontSize["2xl"],
    fontWeight: "600",
    fontVariant: ["tabular-nums"],
  },
})

function HeroImage({ device, active }: { device: Device; active: boolean }) {
  const opacity = useSharedValue(active ? 1 : 0)

  React.useEffect(() => {
    opacity.value = withTiming(active ? 1 : 0, {
      duration: 600,
      reduceMotion: ReduceMotion.System,
    })
  }, [active, opacity])

  const animatedStyle = useAnimatedStyle(() => ({ opacity: opacity.value }))

  return (
    <AnimatedView
      accessible={active}
      accessibilityRole="image"
      accessibilityLabel={device.alt}
      importantForAccessibility={active ? "yes" : "no-hide-descendants"}
      accessibilityElementsHidden={!active}
      style={[styles.layer, animatedStyle]}
    >
      <Image
        source={{ uri: device.image }}
        resizeMode="cover"
        accessibilityIgnoresInvertColors
        style={styles.image}
      />
    </AnimatedView>
  )
}

function PriceCare({
  onAddToBag,
}: {
  onAddToBag?: (selection: {
    device: string
    option: string
    theft: boolean
    total: number
  }) => void
}) {
  const { colors } = useTheme()
  const [device, setDevice] = React.useState(DEVICES[0].name)
  const [option, setOption] = React.useState(DEVICES[0].options[0].label)
  const [theft, setTheft] = React.useState(false)
  const current = DEVICES.find((item) => item.name === device) ?? DEVICES[0]
  const plan =
    current.options.find((item) => item.label === option) ?? current.options[0]
  const withTheft = theft && current.name === "Phone Pro"
  const total = plan.price + (withTheft ? (plan.theft ?? 0) : 0)

  return (
    <Screen edges={["top", "right", "left"]}>
      <ScrollView contentContainerStyle={styles.content}>
        <View style={[styles.hero, { backgroundColor: palette.black }]}>
          {DEVICES.map((item) => (
            <HeroImage
              key={item.name}
              device={item}
              active={item.name === device}
            />
          ))}
          <View
            style={[styles.band, { backgroundColor: "rgba(0, 0, 0, 0.5)" }]}
          >
            <Text style={[styles.kicker, { color: palette.white }]}>
              suiss Care+
            </Text>
            <Text
              role="heading"
              style={[styles.heading, { color: palette.white }]}
            >
              Protection against accidents.
            </Text>
          </View>
        </View>
        <AnimatedView
          entering={FadeInDown.duration(600).reduceMotion(ReduceMotion.System)}
          style={styles.body}
        >
          <Text role="heading" style={styles.heading}>
            Choose your device.
          </Text>
          <ToggleGroup
            value={[device]}
            onValueChange={(value) => {
              const next = DEVICES.find((item) => item.name === value[0])
              if (!next) return
              setDevice(next.name)
              setOption(next.options[0].label)
            }}
            accessibilityLabel="Device"
            style={styles.devices}
          >
            {DEVICES.map((item) => {
              const selected = item.name === device
              return (
                <ToggleGroupItem
                  key={item.name}
                  value={item.name}
                  style={[
                    styles.device,
                    {
                      backgroundColor: selected
                        ? colors.foreground
                        : colors.secondary,
                    },
                  ]}
                >
                  <Text
                    style={[
                      fontSize.sm,
                      {
                        color: selected ? colors.background : colors.foreground,
                      },
                    ]}
                  >
                    {item.name}
                  </Text>
                </ToggleGroupItem>
              )
            })}
          </ToggleGroup>
          <RadioGroup
            key={device}
            value={option}
            onValueChange={setOption}
            accessibilityLabel="Payment"
            style={styles.options}
          >
            {current.options.map((item) => {
              const checked = item.label === option
              return (
                <Pressable
                  key={item.label}
                  onPress={() => setOption(item.label)}
                  style={({ pressed }) => [
                    styles.option,
                    { borderColor: checked ? colors.primary : colors.border },
                    pressed && !checked && { opacity: 0.8 },
                  ]}
                >
                  <RadioGroupItem
                    value={item.label}
                    accessibilityLabel={`${item.label} payment, ${money(item.price)}`}
                    style={styles.radio}
                  />
                  <View style={styles.optionText}>
                    <Text style={styles.strong}>{item.label} payment</Text>
                    <Text
                      style={[fontSize.sm, { color: colors.mutedForeground }]}
                    >
                      Unlimited accidental damage repairs
                    </Text>
                  </View>
                  <Text style={styles.price}>{money(item.price)}</Text>
                </Pressable>
              )
            })}
          </RadioGroup>
          {current.name === "Phone Pro" ? (
            <Pressable
              accessible={false}
              onPress={() => setTheft((value) => !value)}
              style={styles.theft}
            >
              <Switch
                checked={theft}
                onCheckedChange={setTheft}
                accessibilityLabelledBy="price-care-theft"
              />
              <Text
                nativeID="price-care-theft"
                style={[fontSize.sm, { flex: 1 }]}
              >
                Add theft and loss protection
              </Text>
            </Pressable>
          ) : null}
        </AnimatedView>
      </ScrollView>
      <BottomBar separator>
        <BottomBarContent style={styles.bar}>
          <View accessibilityLiveRegion="polite" style={{ flex: 1 }}>
            <Text style={[fontSize.sm, { color: colors.mutedForeground }]}>
              Total
            </Text>
            <Text style={styles.total}>{money(total)}</Text>
          </View>
          <Button
            size="lg"
            onPress={() =>
              onAddToBag?.({
                device: current.name,
                option: plan.label,
                theft: withTheft,
                total,
              })
            }
          >
            <Text>Add to Bag</Text>
          </Button>
        </BottomBarContent>
      </BottomBar>
    </Screen>
  )
}

export { PriceCare }
```

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

- `swift/Components/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)

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

- `swift/Components/ToggleGroup.swift`: see [Toggle Group](https://ui.suiss.app/docs/components/toggle-group.md)

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/PriceCare.kt`: shown above

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

- `kotlin/components/ToggleGroup.kt`: see [Toggle Group](https://ui.suiss.app/docs/components/toggle-group.md)

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

## Usage

```tsx
import { PriceCare } from "@/components/blocks/price-care"
```

```tsx
<PriceCare />
```

Replace `DEVICES` with your products, options and prices. Wire `onAddToBag`, which receives the device, option, theft add-on and total. The photos are Unsplash placeholders.

