# Dash Health

A health summary dashboard with pinned metric cards, sparklines, and category views in a sidebar.

Dash Health 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/dash-health

**React**

**Example**

```tsx title="components/blocks/dash-health.tsx"
"use client"

import {
  CaretRightIcon,
  ChartBarIcon,
  FlameIcon,
  HeartIcon,
  MinusIcon,
  MoonIcon,
  PlusIcon,
  UsersIcon,
} from "@phosphor-icons/react"
import * as React from "react"

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

type Category = "Activity" | "Heart" | "Sleep" | "Mobility"

type CategoryStyle = {
  value: Category
  icon: React.ReactNode
  tint: string
  text: string
}

const categories: [CategoryStyle, ...CategoryStyle[]] = [
  {
    value: "Activity",
    icon: (
      <FlameIcon className="fill-current" />
    ),
    tint: "bg-orange-500",
    text: "text-[color-mix(in_oklab,var(--color-orange-500),var(--foreground)_45%)] dark:text-orange-500",
  },
  {
    value: "Heart",
    icon: (
      <HeartIcon className="fill-current" />
    ),
    tint: "bg-red-500",
    text: "text-[color-mix(in_oklab,var(--color-red-500),var(--foreground)_25%)] dark:text-red-500",
  },
  {
    value: "Sleep",
    icon: (
      <MoonIcon className="fill-current" />
    ),
    tint: "bg-cyan-500",
    text: "text-[color-mix(in_oklab,var(--color-cyan-500),var(--foreground)_50%)] dark:text-cyan-500",
  },
  {
    value: "Mobility",
    icon: (
      <ChartBarIcon />
    ),
    tint: "bg-green-500",
    text: "text-[color-mix(in_oklab,var(--color-green-500),var(--foreground)_45%)] dark:text-green-500",
  },
]

const metrics: {
  category: Category
  label: string
  value: string
  unit?: string
  color: string
  data: number[]
  bars?: boolean
}[] = [
  {
    category: "Activity",
    label: "Move",
    value: "486",
    unit: "kcal",
    color: "var(--color-red-500)",
    data: [40, 62, 38, 70, 55, 81, 65],
    bars: true,
  },
  {
    category: "Activity",
    label: "Steps",
    value: "8,412",
    unit: "steps",
    color: "var(--color-orange-500)",
    data: [60, 80, 45, 90, 70, 95, 52],
    bars: true,
  },
  {
    category: "Heart",
    label: "Resting Heart Rate",
    value: "58",
    unit: "bpm",
    color: "var(--color-red-500)",
    data: [61, 60, 59, 58, 60, 57, 58],
  },
  {
    category: "Sleep",
    label: "Sleep",
    value: "7 hr 12 min",
    color: "var(--color-cyan-500)",
    data: [68, 74, 59, 79, 64, 86, 72],
    bars: true,
  },
  {
    category: "Heart",
    label: "Cardio Fitness",
    value: "42.1",
    unit: "VO₂ max",
    color: "var(--color-red-500)",
    data: [40, 40, 41, 41, 42, 42, 42],
  },
  {
    category: "Mobility",
    label: "Walking Speed",
    value: "5.1",
    unit: "km/h",
    color: "var(--color-green-500)",
    data: [48, 50, 49, 51, 50, 52, 51],
  },
]

function Tile({ icon, tint }: { icon: React.ReactNode; tint: string }) {
  return (
    <span
      className={`flex size-6 shrink-0 items-center justify-center rounded-md text-white [&_svg]:size-3.5 ${tint}`}
    >
      {icon}
    </span>
  )
}

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

function NavSidebar({
  items,
  value,
  onValueChange,
  header,
  footer,
}: {
  items: NavItem[]
  value: string
  onValueChange: (value: string) => void
  header?: React.ReactNode
  footer?: React.ReactNode
}) {
  const { isMobile, setOpenMobile } = useSidebar()
  const groups = [...new Set(items.map((item) => item.group ?? ""))]

  return (
    <Sidebar collapsible="icon">
      {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={() => {
                          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: "Summary",
    label: "Summary",
    icon: (
      <Tile
        icon={
          <HeartIcon className="fill-current" />
        }
        tint="bg-pink-500"
      />
    ),
  },
  {
    value: "Sharing",
    label: "Sharing",
    icon: (
      <Tile
        icon={
          <UsersIcon />
        }
        tint="bg-blue-500"
      />
    ),
  },
  ...categories.map((category) => ({
    value: category.value,
    label: category.value,
    icon: <Tile icon={category.icon} tint={category.tint} />,
    group: "Categories",
  })),
]

const days = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]

function Spark({
  data,
  color,
  bars,
}: {
  data: number[]
  color: string
  bars?: boolean
}) {
  const max = Math.max(...data)
  const min = Math.min(...data)

  if (bars) {
    return (
      <span aria-hidden className="flex h-9 items-end gap-1">
        {data.map((value, index) => (
          <span
            key={days[index]}
            style={{ height: `${(value / max) * 100}%`, background: color }}
            className="w-2 rounded-[3px] opacity-45 last:opacity-100"
          />
        ))}
      </span>
    )
  }

  const points = data
    .map(
      (value, index) =>
        `${((index / (data.length - 1)) * 120).toFixed(1)},${(36 - ((value - min) / Math.max(1, max - min)) * 30 - 3).toFixed(1)}`
    )
    .join(" ")

  return (
    <svg
      aria-hidden
      width="120"
      height="36"
      viewBox="0 0 120 36"
      className="overflow-visible"
    >
      <polyline
        points={points}
        fill="none"
        stroke={color}
        strokeWidth={2.2}
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  )
}

export function DashHealth() {
  const [section, setSection] = React.useState("Summary")
  const [editing, setEditing] = React.useState(false)
  const [pinned, setPinned] = React.useState<string[]>([
    "Move",
    "Steps",
    "Resting Heart Rate",
    "Sleep",
  ])
  const summary = section === "Summary"
  const cards = metrics.filter((metric) =>
    summary
      ? editing || pinned.includes(metric.label)
      : metric.category === section
  )

  return (
    <SidebarProvider>
      <NavSidebar
        items={items}
        value={section}
        onValueChange={(value) => {
          setSection(value)
          setEditing(false)
        }}
        header={
          <p className="px-2 pt-1 text-2xl font-bold tracking-tight">Health</p>
        }
      />
      <SidebarInset className="bg-muted dark:bg-background">
        <div key={section} className="flex flex-col gap-2.5 px-5 py-6 md:px-7">
          <div className="flex items-center gap-2.5">
            <SidebarTrigger className="-ms-2" />
            <h1 className="flex-1 text-3xl font-bold tracking-tight">
              {section}
            </h1>
            <Avatar className="size-8.5">
              <AvatarFallback className="bg-linear-to-br from-[oklch(0.78_0.1_250)] to-[oklch(0.6_0.15_280)] text-white dark:from-[oklch(0.78_0.1_250)] dark:to-[oklch(0.6_0.15_280)]">
                JR
              </AvatarFallback>
            </Avatar>
          </div>
          <div className="mt-2.5 flex items-center gap-2">
            <h2 className="flex-1 text-xl font-bold">
              {summary ? "Pinned" : "Last 7 days"}
            </h2>
            {summary && (
              <Button
                variant="plain"
                size="sm"
                onClick={() => setEditing(!editing)}
                data-editing={editing ? "" : undefined}
                className="text-base data-editing:font-semibold"
              >
                {editing ? "Done" : "Edit"}
              </Button>
            )}
          </div>
          {cards.length > 0 ? (
            <ul className="flex flex-col gap-2.5">
              {cards.map((metric, index) => {
                const category =
                  categories.find((item) => item.value === metric.category) ??
                  categories[0]
                const isPinned = pinned.includes(metric.label)
                return (
                  <li
                    key={metric.label}
                    style={{ transitionDelay: `${index * 40}ms` }}
                    className="flex items-center gap-3 transition-[opacity,translate] duration-800 ease-[cubic-bezier(0.32,0.72,0,1)] starting:translate-y-2 starting:opacity-0 motion-reduce:transition-none"
                  >
                    {editing && (
                      <Button
                        size="icon-xs"
                        aria-label={`${isPinned ? "Unpin" : "Pin"} ${metric.label}`}
                        onClick={() =>
                          setPinned((current) =>
                            isPinned
                              ? current.filter(
                                  (label) => label !== metric.label
                                )
                              : [...current, metric.label]
                          )
                        }
                        data-pinned={isPinned ? "" : undefined}
                        className="relative bg-[color-mix(in_oklab,var(--color-green-500),var(--foreground)_35%)] text-white after:absolute after:-inset-2.5 after:content-[''] hover:bg-[color-mix(in_oklab,var(--color-green-500),var(--foreground)_45%)] data-pinned:bg-destructive data-pinned:hover:bg-destructive/90"
                      >
                        {isPinned ? (
                          <MinusIcon />
                        ) : (
                          <PlusIcon />
                        )}
                      </Button>
                    )}
                    <a
                      href={`#${metric.label.toLowerCase().replaceAll(" ", "-")}`}
                      data-dimmed={editing && !isPinned ? "" : undefined}
                      className="flex min-w-0 flex-1 flex-col gap-2.5 rounded-xl bg-background px-4 py-3 outline-none transition-opacity duration-200 focus-visible:ring-3 focus-visible:ring-ring/50 data-dimmed:opacity-55 dark:bg-muted"
                    >
                      <span
                        className={`flex items-center gap-1.5 text-base font-semibold [&_svg:not([class*='size-'])]:size-4 ${category.text}`}
                      >
                        {category.icon}
                        <span className="flex-1">{metric.label}</span>
                        <span className="text-sm font-normal text-muted-foreground">
                          9:41
                        </span>
                        <CaretRightIcon
                          className="size-3 text-muted-foreground rtl:rotate-180"
                        />
                      </span>
                      <span className="flex items-end gap-4">
                        <span className="flex-1">
                          <span className="text-3xl font-bold tracking-tight tabular-nums">
                            {metric.value}
                          </span>
                          {metric.unit && (
                            <span className="ms-1 text-base font-semibold text-muted-foreground">
                              {metric.unit}
                            </span>
                          )}
                        </span>
                        <Spark
                          data={metric.data}
                          color={metric.color}
                          bars={metric.bars}
                        />
                      </span>
                    </a>
                  </li>
                )
              })}
            </ul>
          ) : (
            <p className="py-7.5 text-center text-base text-muted-foreground">
              No pinned items. Use Edit to add some.
            </p>
          )}
        </div>
      </SidebarInset>
    </SidebarProvider>
  )
}
```

A health summary in the style of a phone health app, with cards for pinned metrics such as Move, Steps and Sleep, each showing its value and a small sparkline or bar chart. Edit mode lets users pin and unpin cards, and each category page lists all its metrics for the last seven days. Navigation is a `Sidebar` that collapses to icons on desktop and opens as a sheet from the trigger on mobile. Use it for fitness, wellness or any personal metrics dashboard.

## Installation

### CLI

```bash title="Terminal"
npx shadcn@latest add @esuiss/dash-health
```

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 dash-health
```

### 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), [Sidebar](https://ui.suiss.app/docs/components/sidebar).

3. Copy the source into `components/blocks/dash-health.tsx`:

```tsx title="components/blocks/dash-health.tsx"
"use client"

import {
  CaretRightIcon,
  ChartBarIcon,
  FlameIcon,
  HeartIcon,
  MinusIcon,
  MoonIcon,
  PlusIcon,
  UsersIcon,
} from "@phosphor-icons/react"
import * as React from "react"

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

type Category = "Activity" | "Heart" | "Sleep" | "Mobility"

type CategoryStyle = {
  value: Category
  icon: React.ReactNode
  tint: string
  text: string
}

const categories: [CategoryStyle, ...CategoryStyle[]] = [
  {
    value: "Activity",
    icon: (
      <FlameIcon className="fill-current" />
    ),
    tint: "bg-orange-500",
    text: "text-[color-mix(in_oklab,var(--color-orange-500),var(--foreground)_45%)] dark:text-orange-500",
  },
  {
    value: "Heart",
    icon: (
      <HeartIcon className="fill-current" />
    ),
    tint: "bg-red-500",
    text: "text-[color-mix(in_oklab,var(--color-red-500),var(--foreground)_25%)] dark:text-red-500",
  },
  {
    value: "Sleep",
    icon: (
      <MoonIcon className="fill-current" />
    ),
    tint: "bg-cyan-500",
    text: "text-[color-mix(in_oklab,var(--color-cyan-500),var(--foreground)_50%)] dark:text-cyan-500",
  },
  {
    value: "Mobility",
    icon: (
      <ChartBarIcon />
    ),
    tint: "bg-green-500",
    text: "text-[color-mix(in_oklab,var(--color-green-500),var(--foreground)_45%)] dark:text-green-500",
  },
]

const metrics: {
  category: Category
  label: string
  value: string
  unit?: string
  color: string
  data: number[]
  bars?: boolean
}[] = [
  {
    category: "Activity",
    label: "Move",
    value: "486",
    unit: "kcal",
    color: "var(--color-red-500)",
    data: [40, 62, 38, 70, 55, 81, 65],
    bars: true,
  },
  {
    category: "Activity",
    label: "Steps",
    value: "8,412",
    unit: "steps",
    color: "var(--color-orange-500)",
    data: [60, 80, 45, 90, 70, 95, 52],
    bars: true,
  },
  {
    category: "Heart",
    label: "Resting Heart Rate",
    value: "58",
    unit: "bpm",
    color: "var(--color-red-500)",
    data: [61, 60, 59, 58, 60, 57, 58],
  },
  {
    category: "Sleep",
    label: "Sleep",
    value: "7 hr 12 min",
    color: "var(--color-cyan-500)",
    data: [68, 74, 59, 79, 64, 86, 72],
    bars: true,
  },
  {
    category: "Heart",
    label: "Cardio Fitness",
    value: "42.1",
    unit: "VO₂ max",
    color: "var(--color-red-500)",
    data: [40, 40, 41, 41, 42, 42, 42],
  },
  {
    category: "Mobility",
    label: "Walking Speed",
    value: "5.1",
    unit: "km/h",
    color: "var(--color-green-500)",
    data: [48, 50, 49, 51, 50, 52, 51],
  },
]

function Tile({ icon, tint }: { icon: React.ReactNode; tint: string }) {
  return (
    <span
      className={`flex size-6 shrink-0 items-center justify-center rounded-md text-white [&_svg]:size-3.5 ${tint}`}
    >
      {icon}
    </span>
  )
}

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

function NavSidebar({
  items,
  value,
  onValueChange,
  header,
  footer,
}: {
  items: NavItem[]
  value: string
  onValueChange: (value: string) => void
  header?: React.ReactNode
  footer?: React.ReactNode
}) {
  const { isMobile, setOpenMobile } = useSidebar()
  const groups = [...new Set(items.map((item) => item.group ?? ""))]

  return (
    <Sidebar collapsible="icon">
      {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={() => {
                          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: "Summary",
    label: "Summary",
    icon: (
      <Tile
        icon={
          <HeartIcon className="fill-current" />
        }
        tint="bg-pink-500"
      />
    ),
  },
  {
    value: "Sharing",
    label: "Sharing",
    icon: (
      <Tile
        icon={
          <UsersIcon />
        }
        tint="bg-blue-500"
      />
    ),
  },
  ...categories.map((category) => ({
    value: category.value,
    label: category.value,
    icon: <Tile icon={category.icon} tint={category.tint} />,
    group: "Categories",
  })),
]

const days = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]

function Spark({
  data,
  color,
  bars,
}: {
  data: number[]
  color: string
  bars?: boolean
}) {
  const max = Math.max(...data)
  const min = Math.min(...data)

  if (bars) {
    return (
      <span aria-hidden className="flex h-9 items-end gap-1">
        {data.map((value, index) => (
          <span
            key={days[index]}
            style={{ height: `${(value / max) * 100}%`, background: color }}
            className="w-2 rounded-[3px] opacity-45 last:opacity-100"
          />
        ))}
      </span>
    )
  }

  const points = data
    .map(
      (value, index) =>
        `${((index / (data.length - 1)) * 120).toFixed(1)},${(36 - ((value - min) / Math.max(1, max - min)) * 30 - 3).toFixed(1)}`
    )
    .join(" ")

  return (
    <svg
      aria-hidden
      width="120"
      height="36"
      viewBox="0 0 120 36"
      className="overflow-visible"
    >
      <polyline
        points={points}
        fill="none"
        stroke={color}
        strokeWidth={2.2}
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  )
}

export function DashHealth() {
  const [section, setSection] = React.useState("Summary")
  const [editing, setEditing] = React.useState(false)
  const [pinned, setPinned] = React.useState<string[]>([
    "Move",
    "Steps",
    "Resting Heart Rate",
    "Sleep",
  ])
  const summary = section === "Summary"
  const cards = metrics.filter((metric) =>
    summary
      ? editing || pinned.includes(metric.label)
      : metric.category === section
  )

  return (
    <SidebarProvider>
      <NavSidebar
        items={items}
        value={section}
        onValueChange={(value) => {
          setSection(value)
          setEditing(false)
        }}
        header={
          <p className="px-2 pt-1 text-2xl font-bold tracking-tight">Health</p>
        }
      />
      <SidebarInset className="bg-muted dark:bg-background">
        <div key={section} className="flex flex-col gap-2.5 px-5 py-6 md:px-7">
          <div className="flex items-center gap-2.5">
            <SidebarTrigger className="-ms-2" />
            <h1 className="flex-1 text-3xl font-bold tracking-tight">
              {section}
            </h1>
            <Avatar className="size-8.5">
              <AvatarFallback className="bg-linear-to-br from-[oklch(0.78_0.1_250)] to-[oklch(0.6_0.15_280)] text-white dark:from-[oklch(0.78_0.1_250)] dark:to-[oklch(0.6_0.15_280)]">
                JR
              </AvatarFallback>
            </Avatar>
          </div>
          <div className="mt-2.5 flex items-center gap-2">
            <h2 className="flex-1 text-xl font-bold">
              {summary ? "Pinned" : "Last 7 days"}
            </h2>
            {summary && (
              <Button
                variant="plain"
                size="sm"
                onClick={() => setEditing(!editing)}
                data-editing={editing ? "" : undefined}
                className="text-base data-editing:font-semibold"
              >
                {editing ? "Done" : "Edit"}
              </Button>
            )}
          </div>
          {cards.length > 0 ? (
            <ul className="flex flex-col gap-2.5">
              {cards.map((metric, index) => {
                const category =
                  categories.find((item) => item.value === metric.category) ??
                  categories[0]
                const isPinned = pinned.includes(metric.label)
                return (
                  <li
                    key={metric.label}
                    style={{ transitionDelay: `${index * 40}ms` }}
                    className="flex items-center gap-3 transition-[opacity,translate] duration-800 ease-[cubic-bezier(0.32,0.72,0,1)] starting:translate-y-2 starting:opacity-0 motion-reduce:transition-none"
                  >
                    {editing && (
                      <Button
                        size="icon-xs"
                        aria-label={`${isPinned ? "Unpin" : "Pin"} ${metric.label}`}
                        onClick={() =>
                          setPinned((current) =>
                            isPinned
                              ? current.filter(
                                  (label) => label !== metric.label
                                )
                              : [...current, metric.label]
                          )
                        }
                        data-pinned={isPinned ? "" : undefined}
                        className="relative bg-[color-mix(in_oklab,var(--color-green-500),var(--foreground)_35%)] text-white after:absolute after:-inset-2.5 after:content-[''] hover:bg-[color-mix(in_oklab,var(--color-green-500),var(--foreground)_45%)] data-pinned:bg-destructive data-pinned:hover:bg-destructive/90"
                      >
                        {isPinned ? (
                          <MinusIcon />
                        ) : (
                          <PlusIcon />
                        )}
                      </Button>
                    )}
                    <a
                      href={`#${metric.label.toLowerCase().replaceAll(" ", "-")}`}
                      data-dimmed={editing && !isPinned ? "" : undefined}
                      className="flex min-w-0 flex-1 flex-col gap-2.5 rounded-xl bg-background px-4 py-3 outline-none transition-opacity duration-200 focus-visible:ring-3 focus-visible:ring-ring/50 data-dimmed:opacity-55 dark:bg-muted"
                    >
                      <span
                        className={`flex items-center gap-1.5 text-base font-semibold [&_svg:not([class*='size-'])]:size-4 ${category.text}`}
                      >
                        {category.icon}
                        <span className="flex-1">{metric.label}</span>
                        <span className="text-sm font-normal text-muted-foreground">
                          9:41
                        </span>
                        <CaretRightIcon
                          className="size-3 text-muted-foreground rtl:rotate-180"
                        />
                      </span>
                      <span className="flex items-end gap-4">
                        <span className="flex-1">
                          <span className="text-3xl font-bold tracking-tight tabular-nums">
                            {metric.value}
                          </span>
                          {metric.unit && (
                            <span className="ms-1 text-base font-semibold text-muted-foreground">
                              {metric.unit}
                            </span>
                          )}
                        </span>
                        <Spark
                          data={metric.data}
                          color={metric.color}
                          bars={metric.bars}
                        />
                      </span>
                    </a>
                  </li>
                )
              })}
            </ul>
          ) : (
            <p className="py-7.5 text-center text-base text-muted-foreground">
              No pinned items. Use Edit to add some.
            </p>
          )}
        </div>
      </SidebarInset>
    </SidebarProvider>
  )
}
```

4. Update the import paths to match your project.

## Usage

```tsx
import { DashHealth } from "@/components/blocks/dash-health"
```

```tsx
<DashHealth />
```

Edit `categories` for names, icons and colors, and `metrics` for the cards; each metric has a category, label, value, optional unit, color and seven data points. Store the pinned list per user instead of in local state. Card links point to anchors built from each label, and the 9:41 time and JR avatar are hard-coded.

## Notes

Sparklines are hidden from assistive technology because each value is also shown as text, and pin buttons are labeled with the metric name.

**React Native, SwiftUI, Jetpack Compose**

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

```tsx title="uniwind/components/blocks/dash-health.tsx"
import { cn } from "cn"
import {
  CaretRightIcon,
  ChartBarIcon,
  FlameIcon,
  HeartIcon,
  MinusIcon,
  MoonIcon,
  type Icon as PhosphorIcon,
  type IconProps as PhosphorIconProps,
  PlusIcon,
  UsersIcon,
} from "phosphor-react-native"
import * as React from "react"
import { I18nManager, Pressable, View } from "react-native"
import { FadeInDown, ReduceMotion } from "react-native-reanimated"
import Svg, { Polyline } from "react-native-svg"
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 {
  Item,
  ItemActions,
  ItemContent,
  ItemGroup,
  ItemMedia,
  ItemSeparator,
  ItemTitle,
} from "@/components/ui/item"
import { Screen } from "@/components/ui/screen"
import { Text } from "@/components/ui/text"

type Category = "Activity" | "Heart" | "Sleep" | "Mobility"

type Section = "Summary" | "Sharing" | Category

type CategoryStyle = {
  value: Category
  icon: PhosphorIcon
  tint: string
  text: string
}

const CATEGORIES: [CategoryStyle, ...CategoryStyle[]] = [
  {
    value: "Activity",
    icon: FlameIcon,
    tint: "bg-orange-500",
    text: "text-orange-500",
  },
  { value: "Heart", icon: HeartIcon, tint: "bg-red-500", text: "text-red-500" },
  {
    value: "Sleep",
    icon: MoonIcon,
    tint: "bg-cyan-500",
    text: "text-cyan-500",
  },
  {
    value: "Mobility",
    icon: ChartBarIcon,
    tint: "bg-green-500",
    text: "text-green-500",
  },
]

const SECTIONS: { value: Section; icon: PhosphorIcon; tint: string }[] = [
  { value: "Summary", icon: HeartIcon, tint: "bg-pink-500" },
  { value: "Sharing", icon: UsersIcon, tint: "bg-blue-500" },
]

type Metric = {
  category: Category
  label: string
  value: string
  unit?: string
  bar: string
  line: string
  data: number[]
  bars?: boolean
}

const METRICS: Metric[] = [
  {
    category: "Activity",
    label: "Move",
    value: "486",
    unit: "kcal",
    bar: "bg-red-500",
    line: "text-red-500",
    data: [40, 62, 38, 70, 55, 81, 65],
    bars: true,
  },
  {
    category: "Activity",
    label: "Steps",
    value: "8,412",
    unit: "steps",
    bar: "bg-orange-500",
    line: "text-orange-500",
    data: [60, 80, 45, 90, 70, 95, 52],
    bars: true,
  },
  {
    category: "Heart",
    label: "Resting Heart Rate",
    value: "58",
    unit: "bpm",
    bar: "bg-red-500",
    line: "text-red-500",
    data: [61, 60, 59, 58, 60, 57, 58],
  },
  {
    category: "Sleep",
    label: "Sleep",
    value: "7 hr 12 min",
    bar: "bg-cyan-500",
    line: "text-cyan-500",
    data: [68, 74, 59, 79, 64, 86, 72],
    bars: true,
  },
  {
    category: "Heart",
    label: "Cardio Fitness",
    value: "42.1",
    unit: "VO₂ max",
    bar: "bg-red-500",
    line: "text-red-500",
    data: [40, 40, 41, 41, 42, 42, 42],
  },
  {
    category: "Mobility",
    label: "Walking Speed",
    value: "5.1",
    unit: "km/h",
    bar: "bg-green-500",
    line: "text-green-500",
    data: [48, 50, 49, 51, 50, 52, 51],
  },
]

const DAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]

const SparkContext = React.createContext("")

function SparkLine({ color }: PhosphorIconProps) {
  const points = React.useContext(SparkContext)
  return (
    <Svg width={120} height={36} viewBox="0 0 120 36">
      <Polyline
        points={points}
        fill="none"
        stroke={color}
        strokeWidth={2.2}
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </Svg>
  )
}

function Spark({ metric }: { metric: Metric }) {
  const max = Math.max(...metric.data)
  const min = Math.min(...metric.data)

  if (metric.bars) {
    return (
      <View
        accessibilityElementsHidden
        importantForAccessibility="no-hide-descendants"
        className="h-9 flex-row items-end gap-1"
      >
        {metric.data.map((value, index) => (
          <View
            key={DAYS[index]}
            style={{ height: `${(value / max) * 100}%` }}
            className={cn(
              "w-2 rounded-[3px]",
              metric.bar,
              index < metric.data.length - 1 && "opacity-45"
            )}
          />
        ))}
      </View>
    )
  }

  const points = metric.data
    .map(
      (value, index) =>
        `${((index / (metric.data.length - 1)) * 120).toFixed(1)},${(36 - ((value - min) / Math.max(1, max - min)) * 30 - 3).toFixed(1)}`
    )
    .join(" ")

  return (
    <View
      accessibilityElementsHidden
      importantForAccessibility="no-hide-descendants"
    >
      <SparkContext.Provider value={points}>
        <Icon as={SparkLine} className={cn("size-9", metric.line)} />
      </SparkContext.Provider>
    </View>
  )
}

function Tile({ icon, tint }: { icon: PhosphorIcon; tint: string }) {
  return (
    <ItemMedia className={cn("size-7.25 rounded-[7px]", tint)}>
      <Icon
        as={icon}
        weight="fill"
        className="size-4 text-primary-foreground"
      />
    </ItemMedia>
  )
}

function Chevron({ className }: { className?: string }) {
  return (
    <Icon
      as={CaretRightIcon}
      weight="bold"
      className={cn(
        "size-3.5 text-muted-foreground",
        I18nManager.isRTL && "rotate-180",
        className
      )}
    />
  )
}

function MetricCard({
  metric,
  index,
  editing,
  pinned,
  onTogglePin,
  onPress,
}: {
  metric: Metric
  index: number
  editing: boolean
  pinned: boolean
  onTogglePin: () => void
  onPress: () => void
}) {
  const category =
    CATEGORIES.find((item) => item.value === metric.category) ?? CATEGORIES[0]
  return (
    <AnimatedView
      entering={FadeInDown.delay(index * 40)
        .duration(800)
        .reduceMotion(ReduceMotion.System)}
      className="flex-row items-center gap-3"
    >
      {editing ? (
        <Button
          size="icon-xs"
          accessibilityLabel={`${pinned ? "Unpin" : "Pin"} ${metric.label}`}
          hitSlop={10}
          onPress={onTogglePin}
          className={pinned ? "bg-destructive" : "bg-green-500"}
        >
          <Icon
            as={pinned ? MinusIcon : PlusIcon}
            weight="bold"
            className="size-3.5 text-primary-foreground"
          />
        </Button>
      ) : null}
      <Pressable
        role="button"
        onPress={onPress}
        className={cn(
          "min-w-0 flex-1 gap-2.5 rounded-xl bg-background px-4 py-3 active:opacity-80 dark:bg-muted",
          editing && !pinned && "opacity-55"
        )}
      >
        <View className="flex-row items-center gap-1.5">
          <Icon
            as={category.icon}
            weight="fill"
            className={cn("size-4", category.text)}
          />
          <Text
            numberOfLines={1}
            className={cn("flex-1 text-base font-semibold", category.text)}
          >
            {metric.label}
          </Text>
          <Text className="text-sm text-muted-foreground">9:41</Text>
          <Chevron className="size-3" />
        </View>
        <View className="flex-row items-end gap-4">
          <Text className="flex-1">
            <Text className="text-3xl font-bold tabular-nums">
              {metric.value}
            </Text>
            {metric.unit ? (
              <Text className="text-base font-semibold text-muted-foreground">
                {` ${metric.unit}`}
              </Text>
            ) : null}
          </Text>
          <Spark metric={metric} />
        </View>
      </Pressable>
    </AnimatedView>
  )
}

function SectionView({
  section,
  onMetricPress,
  onPinnedChange,
}: {
  section: Section
  onMetricPress?: (label: string) => void
  onPinnedChange?: (pinned: string[]) => void
}) {
  const [editing, setEditing] = React.useState(false)
  const [pinned, setPinned] = React.useState<string[]>([
    "Move",
    "Steps",
    "Resting Heart Rate",
    "Sleep",
  ])
  const summary = section === "Summary"
  const cards = METRICS.filter((metric) =>
    summary
      ? editing || pinned.includes(metric.label)
      : metric.category === section
  )

  const togglePin = (label: string) => {
    const next = pinned.includes(label)
      ? pinned.filter((item) => item !== label)
      : [...pinned, label]
    setPinned(next)
    onPinnedChange?.(next)
  }

  return (
    <View className="gap-2.5 px-4">
      <View className="mt-2.5 flex-row items-center gap-2">
        <Text role="heading" className="flex-1 text-xl font-bold">
          {summary ? "Pinned" : "Last 7 days"}
        </Text>
        {summary ? (
          <Button
            variant="plain"
            size="sm"
            accessibilityState={{ selected: editing }}
            onPress={() => setEditing(!editing)}
          >
            <Text className={cn("text-base", editing && "font-semibold")}>
              {editing ? "Done" : "Edit"}
            </Text>
          </Button>
        ) : null}
      </View>
      {cards.length > 0 ? (
        <View className="gap-2.5">
          {cards.map((metric, index) => (
            <MetricCard
              key={metric.label}
              metric={metric}
              index={index}
              editing={editing}
              pinned={pinned.includes(metric.label)}
              onTogglePin={() => togglePin(metric.label)}
              onPress={() => onMetricPress?.(metric.label)}
            />
          ))}
        </View>
      ) : (
        <Text className="py-7.5 text-center text-base text-muted-foreground">
          No pinned items. Use Edit to add some.
        </Text>
      )}
    </View>
  )
}

function ProfileAvatar() {
  return (
    <Avatar className="size-8.5">
      <AvatarFallback className="bg-indigo-500">JR</AvatarFallback>
    </Avatar>
  )
}

function SectionRow({
  label,
  icon,
  tint,
  onPress,
}: {
  label: string
  icon: PhosphorIcon
  tint: string
  onPress: () => void
}) {
  return (
    <Item size="sm" onPress={onPress} className="min-h-11 gap-3.5 py-2">
      <Tile icon={icon} tint={tint} />
      <ItemContent>
        <ItemTitle className="font-normal">{label}</ItemTitle>
      </ItemContent>
      <ItemActions>
        <Chevron />
      </ItemActions>
    </Item>
  )
}

function DashHealth({
  onMetricPress,
  onPinnedChange,
}: {
  onMetricPress?: (label: string) => void
  onPinnedChange?: (pinned: string[]) => void
}) {
  const [section, setSection] = React.useState<Section | null>("Summary")
  const scroll = useHeaderScroll()

  return (
    <Screen
      variant="secondary"
      edges={["top", "right", "left"]}
      className="dark:bg-background"
    >
      <Header scroll={scroll}>
        {section ? (
          <HeaderBack onPress={() => setSection(null)}>Health</HeaderBack>
        ) : null}
        <HeaderTitle>{section ?? "Health"}</HeaderTitle>
      </Header>
      <HeaderScrollView key={section ?? "health"} scroll={scroll}>
        <HeaderLargeTitle className="flex-row items-center gap-2.5">
          <Text role="heading" className="flex-1 text-3xl font-bold">
            {section ?? "Health"}
          </Text>
          <ProfileAvatar />
        </HeaderLargeTitle>
        {section ? (
          <SectionView
            section={section}
            onMetricPress={onMetricPress}
            onPinnedChange={onPinnedChange}
          />
        ) : (
          <View className="gap-2 px-4">
            <ItemGroup variant="inset" className="bg-background dark:bg-muted">
              {SECTIONS.map((item, index) => (
                <React.Fragment key={item.value}>
                  {index > 0 ? <ItemSeparator className="ms-14.25" /> : null}
                  <SectionRow
                    label={item.value}
                    icon={item.icon}
                    tint={item.tint}
                    onPress={() => setSection(item.value)}
                  />
                </React.Fragment>
              ))}
            </ItemGroup>
            <Text
              role="heading"
              className="mt-4 px-1 text-xl font-bold text-foreground"
            >
              Categories
            </Text>
            <ItemGroup variant="inset" className="bg-background dark:bg-muted">
              {CATEGORIES.map((item, index) => (
                <React.Fragment key={item.value}>
                  {index > 0 ? <ItemSeparator className="ms-14.25" /> : null}
                  <SectionRow
                    label={item.value}
                    icon={item.icon}
                    tint={item.tint}
                    onPress={() => setSection(item.value)}
                  />
                </React.Fragment>
              ))}
            </ItemGroup>
          </View>
        )}
        <View className="h-8" />
      </HeaderScrollView>
    </Screen>
  )
}

export { DashHealth }
```

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

public struct DashHealth: View {
  struct Category {
    let value: String
    let icon: Image
    let tint: Color
  }

  struct Metric: Identifiable {
    let category: String
    let label: String
    let value: String
    var unit: String?
    let color: Color
    let data: [Double]
    var bars = false

    var id: String { label }
  }

  static var categories: [Category] {
    [
      Category(value: "Activity", icon: Ph.flame.fill, tint: Palette.orange500),
      Category(value: "Heart", icon: Ph.heart.fill, tint: Palette.red500),
      Category(value: "Sleep", icon: Ph.moon.fill, tint: Palette.cyan500),
      Category(value: "Mobility", icon: Ph.chartBar.fill, tint: Palette.green500),
    ]
  }

  static var metrics: [Metric] {
    [
      Metric(category: "Activity", label: "Move", value: "486", unit: "kcal", color: Palette.red500, data: [40, 62, 38, 70, 55, 81, 65], bars: true),
      Metric(category: "Activity", label: "Steps", value: "8,412", unit: "steps", color: Palette.orange500, data: [60, 80, 45, 90, 70, 95, 52], bars: true),
      Metric(category: "Heart", label: "Resting Heart Rate", value: "58", unit: "bpm", color: Palette.red500, data: [61, 60, 59, 58, 60, 57, 58]),
      Metric(category: "Sleep", label: "Sleep", value: "7 hr 12 min", color: Palette.cyan500, data: [68, 74, 59, 79, 64, 86, 72], bars: true),
      Metric(category: "Heart", label: "Cardio Fitness", value: "42.1", unit: "VO₂ max", color: Palette.red500, data: [40, 40, 41, 41, 42, 42, 42]),
      Metric(category: "Mobility", label: "Walking Speed", value: "5.1", unit: "km/h", color: Palette.green500, data: [48, 50, 49, 51, 50, 52, 51]),
    ]
  }

  static let defaultPinned = ["Move", "Steps", "Resting Heart Rate", "Sleep"]

  static func cards(section: String, editing: Bool, pinned: [String]) -> [Metric] {
    metrics.filter { metric in
      section == "Summary" ? editing || pinned.contains(metric.label) : metric.category == section
    }
  }

  private var onMetricPress: ((String) -> Void)?
  private var onPinnedChange: (([String]) -> Void)?
  @State private var section: String?
  @State private var editing: Bool
  @State private var pinned: [String]
  @Environment(\.colorScheme) private var colorScheme
  @Environment(\.accessibilityReduceMotion) private var reduceMotion

  public init(
    onMetricPress: ((_ metric: String) -> Void)? = nil,
    onPinnedChange: ((_ pinned: [String]) -> Void)? = nil
  ) {
    self.init(section: nil)
    self.onMetricPress = onMetricPress
    self.onPinnedChange = onPinnedChange
  }

  init(section: String?, editing: Bool = false, pinned: [String] = DashHealth.defaultPinned) {
    _section = State(initialValue: section)
    _editing = State(initialValue: editing)
    _pinned = State(initialValue: pinned)
  }

  private var dark: Bool { colorScheme == .dark }

  public var body: some View {
    Screen(background: dark ? .background : .muted) {
      ZStack {
        if let section {
          detail(section)
            .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 {
    HeaderScrollView {
      HeaderLargeTitle("Health")
      VStack(alignment: .leading, spacing: 10) {
        group {
          sectionRow("Summary", icon: Ph.heart.fill, tint: Palette.pink500)
          ItemSeparator().padding(.leading, 40)
          sectionRow("Sharing", icon: Ph.users.fill, tint: Palette.blue500)
        }
        Text("Categories")
          .scaledFont(.xl, weight: .bold)
          .padding(.top, 14)
          .accessibilityAddTraits(.isHeader)
        group {
          ForEach(Array(Self.categories.enumerated()), id: \.offset) { index, category in
            if index > 0 {
              ItemSeparator().padding(.leading, 40)
            }
            sectionRow(category.value, icon: category.icon, tint: category.tint)
          }
        }
      }
      .padding(.horizontal, 16)
      .padding(.top, 4)
      .padding(.bottom, 24)
    } header: {
      Header {
        HeaderTitle("Health")
      } trailing: {
        Text(verbatim: "JR")
          .scaledFont(.xs, weight: .semibold)
          .lineLimit(1)
          .minimumScaleFactor(0.5)
          .foregroundStyle(EsuissColor.primaryForeground)
          .frame(width: 34, height: 34)
          .background(Palette.indigo500, in: Circle())
          .padding(.trailing, 8)
          .accessibilityHidden(true)
      }
    }
  }

  private func group<Content: View>(@ViewBuilder content: () -> Content) -> some View {
    let shape = RoundedRectangle(cornerRadius: Radius.xl, style: .continuous)
    return VStack(alignment: .leading, spacing: 0) {
      content()
    }
    .environment(\.itemGroupInset, true)
    .background(dark ? EsuissColor.muted : EsuissColor.background, in: shape)
    .clipShape(shape)
  }

  private func sectionRow(_ value: String, icon: Image, tint: Color) -> some View {
    Item(size: .sm, action: {
      editing = false
      section = value
    }) {
      ItemMedia {
        DashHealthTile(icon: icon, tint: tint)
      }
      ItemContent {
        ItemTitle { Text(LocalizedStringKey(value)).fontWeight(.regular) }
      }
      ItemActions {
        Ph.caretRight.bold
          .frame(width: 12, height: 12)
          .foregroundStyle(EsuissColor.mutedForeground)
          .flipsForRightToLeftLayoutDirection(true)
      }
    }
  }

  private func detail(_ section: String) -> some View {
    let summary = section == "Summary"
    let cards = Self.cards(section: section, editing: editing, pinned: pinned)
    return HeaderScrollView {
      HeaderLargeTitle(LocalizedStringKey(section))
      VStack(alignment: .leading, spacing: 10) {
        HStack(spacing: 8) {
          Text(summary ? "Pinned" : "Last 7 days")
            .scaledFont(.xl, weight: .bold)
            .frame(maxWidth: .infinity, alignment: .leading)
            .accessibilityAddTraits(.isHeader)
          if summary {
            Button {
              withAnimation(reduceMotion ? nil : .timingCurve(0.32, 0.72, 0, 1, duration: 0.4)) { editing.toggle() }
            } label: {
              Text(editing ? "Done" : "Edit")
                .fontWeight(editing ? .semibold : .regular)
            }
            .buttonStyle(.variant(.plain, size: .sm))
          }
        }
        .frame(minHeight: 32)
        .padding(.top, 4)
        if cards.isEmpty {
          Text("No pinned items. Use Edit to add some.")
            .scaledFont(.base)
            .foregroundStyle(EsuissColor.mutedForeground)
            .frame(maxWidth: .infinity)
            .padding(.vertical, 30)
        } else {
          cardList(cards)
        }
      }
      .padding(.horizontal, 16)
      .padding(.bottom, 24)
    } header: {
      Header {
        HeaderTitle(LocalizedStringKey(section))
      } leading: {
        HeaderBack("Health") {
          editing = false
          self.section = nil
        }
      }
    }
  }

  private func cardList(_ cards: [Metric]) -> some View {
    VStack(spacing: 10) {
      ForEach(cards) { metric in
        let isPinned = pinned.contains(metric.label)
        HStack(spacing: 12) {
          if editing && section == "Summary" {
            Button {
              toggle(metric.label)
            } label: {
              (isPinned ? Ph.minus.bold : Ph.plus.bold)
                .frame(width: 12, height: 12)
                .foregroundStyle(EsuissColor.primaryForeground)
                .frame(width: 24, height: 24)
                .background(isPinned ? EsuissColor.destructive : Palette.green500, in: Circle())
                .frame(width: 44, height: 44)
                .contentShape(Rectangle())
            }
            .buttonStyle(.plain)
            .padding(-10)
            .accessibilityLabel(Text(isPinned ? "Unpin \(metric.label)" : "Pin \(metric.label)"))
            .transition(.scale.combined(with: .opacity))
          }
          DashHealthCard(
            metric: metric,
            category: Self.categories.first { $0.value == metric.category } ?? Self.categories[0],
            dimmed: editing && !isPinned
          ) {
            onMetricPress?(metric.label)
          }
        }
        .transition(.opacity.combined(with: .offset(y: 8)))
      }
    }
  }

  private func toggle(_ label: String) {
    withAnimation(reduceMotion ? nil : .timingCurve(0.32, 0.72, 0, 1, duration: 0.3)) {
      if pinned.contains(label) {
        pinned.removeAll { $0 == label }
      } else {
        pinned.append(label)
      }
    }
    onPinnedChange?(pinned)
  }
}

private struct DashHealthTile: View {
  let icon: Image
  let tint: Color

  var body: some View {
    icon
      .frame(width: 14, height: 14)
      .foregroundStyle(EsuissColor.primaryForeground)
      .frame(width: 26, height: 26)
      .background(tint, in: RoundedRectangle(cornerRadius: Radius.md, style: .continuous))
      .accessibilityHidden(true)
  }
}

private struct DashHealthCard: View {
  let metric: DashHealth.Metric
  let category: DashHealth.Category
  let dimmed: Bool
  let action: () -> Void

  @Environment(\.colorScheme) private var colorScheme

  var body: some View {
    Button(action: action) {
      VStack(alignment: .leading, spacing: 10) {
        HStack(spacing: 6) {
          category.icon
            .frame(width: 16, height: 16)
          Text(LocalizedStringKey(metric.label))
            .scaledFont(.base, weight: .semibold)
            .frame(maxWidth: .infinity, alignment: .leading)
          Text(verbatim: "9:41")
            .scaledFont(.sm)
            .foregroundStyle(EsuissColor.mutedForeground)
          Ph.caretRight.bold
            .frame(width: 12, height: 12)
            .foregroundStyle(EsuissColor.mutedForeground)
            .flipsForRightToLeftLayoutDirection(true)
        }
        .foregroundStyle(category.tint)
        HStack(alignment: .bottom, spacing: 16) {
          HStack(alignment: .firstTextBaseline, spacing: 4) {
            Text(verbatim: metric.value)
              .scaledFont(.xl3, weight: .bold)
              .foregroundStyle(EsuissColor.foreground)
            if let unit = metric.unit {
              Text(verbatim: unit)
                .scaledFont(.base, weight: .semibold)
                .foregroundStyle(EsuissColor.mutedForeground)
            }
          }
            .monospacedDigit()
            .lineLimit(1)
            .minimumScaleFactor(0.7)
            .frame(maxWidth: .infinity, alignment: .leading)
          DashHealthSpark(data: metric.data, color: metric.color, bars: metric.bars)
        }
      }
      .padding(.horizontal, 16)
      .padding(.vertical, 12)
      .background(colorScheme == .dark ? EsuissColor.muted : EsuissColor.background, in: RoundedRectangle(cornerRadius: Radius.xl, style: .continuous))
      .contentShape(RoundedRectangle(cornerRadius: Radius.xl, style: .continuous))
      .opacity(dimmed ? 0.55 : 1)
      .animation(.easeOut(duration: 0.2), value: dimmed)
    }
    .buttonStyle(DashHealthCardStyle())
    .accessibilityElement(children: .combine)
  }
}

private struct DashHealthCardStyle: ButtonStyle {
  func makeBody(configuration: Configuration) -> some View {
    configuration.label
      .opacity(configuration.isPressed ? 0.8 : 1)
  }
}

private struct DashHealthSpark: View {
  let data: [Double]
  let color: Color
  let bars: Bool

  var body: some View {
    let high = data.max() ?? 1
    let low = data.min() ?? 0
    Group {
      if bars {
        HStack(alignment: .bottom, spacing: 4) {
          ForEach(Array(data.enumerated()), id: \.offset) { index, value in
            RoundedRectangle(cornerRadius: Radius.xs, style: .continuous)
              .fill(color)
              .opacity(index == data.count - 1 ? 1 : 0.45)
              .frame(width: 8, height: max(2, 36 * value / max(high, 1)))
          }
        }
        .frame(height: 36, alignment: .bottom)
      } else {
        Path { path in
          for (index, value) in data.enumerated() {
            let x = CGFloat(index) / CGFloat(max(data.count - 1, 1)) * 120
            let y = 36 - CGFloat((value - low) / max(1, high - low)) * 30 - 3
            if index == 0 {
              path.move(to: CGPoint(x: x, y: y))
            } else {
              path.addLine(to: CGPoint(x: x, y: y))
            }
          }
        }
        .stroke(color, style: StrokeStyle(lineWidth: 2.2, lineCap: .round, lineJoin: .round))
        .frame(width: 120, height: 36)
      }
    }
    .accessibilityHidden(true)
  }
}
```

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

import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
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.key
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.nestedscroll.nestedScroll
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.semantics
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Bold
import com.adamglin.phosphoricons.Fill
import com.adamglin.phosphoricons.bold.CaretRight
import com.adamglin.phosphoricons.bold.Minus
import com.adamglin.phosphoricons.bold.Plus
import com.adamglin.phosphoricons.fill.ChartBar
import com.adamglin.phosphoricons.fill.Flame
import com.adamglin.phosphoricons.fill.Heart
import com.adamglin.phosphoricons.fill.Moon
import com.adamglin.phosphoricons.fill.Users
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.HeaderActions
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.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.Screen
import com.esuiss.ui.components.ScreenVariant
import com.esuiss.ui.components.rememberHeaderState
import com.esuiss.ui.theme.Colors
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Palette
import com.esuiss.ui.theme.Radius
import com.esuiss.ui.theme.Typography

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

private class HealthCategory(
    val value: String,
    val icon: ImageVector,
    val tint: (Colors) -> Color,
    val mix: Float,
)

private val HealthCategories = listOf(
    HealthCategory("Activity", PhosphorIcons.Fill.Flame, { Palette.orange500 }, 0.45f),
    HealthCategory("Heart", PhosphorIcons.Fill.Heart, { Palette.red500 }, 0.25f),
    HealthCategory("Sleep", PhosphorIcons.Fill.Moon, { Palette.cyan500 }, 0.5f),
    HealthCategory("Mobility", PhosphorIcons.Fill.ChartBar, { Palette.green500 }, 0.45f),
)

private class HealthMetric(
    val category: String,
    val label: String,
    val value: String,
    val unit: String?,
    val color: (Colors) -> Color,
    val data: List<Int>,
    val bars: Boolean,
)

private val HealthMetrics = listOf(
    HealthMetric("Activity", "Move", "486", "kcal", { Palette.red500 }, listOf(40, 62, 38, 70, 55, 81, 65), true),
    HealthMetric("Activity", "Steps", "8,412", "steps", { Palette.orange500 }, listOf(60, 80, 45, 90, 70, 95, 52), true),
    HealthMetric("Heart", "Resting Heart Rate", "58", "bpm", { Palette.red500 }, listOf(61, 60, 59, 58, 60, 57, 58), false),
    HealthMetric("Sleep", "Sleep", "7 hr 12 min", null, { Palette.cyan500 }, listOf(68, 74, 59, 79, 64, 86, 72), true),
    HealthMetric("Heart", "Cardio Fitness", "42.1", "VO₂ max", { Palette.red500 }, listOf(40, 40, 41, 41, 42, 42, 42), false),
    HealthMetric("Mobility", "Walking Speed", "5.1", "km/h", { Palette.green500 }, listOf(48, 50, 49, 51, 50, 52, 51), false),
)

private fun categoryText(colors: Colors, category: HealthCategory): Color =
    if (colors.isDark) category.tint(colors) else lerp(category.tint(colors), colors.foreground, category.mix)

@Composable
fun DashHealth(
    modifier: Modifier = Modifier,
    onPinnedChange: (pinned: List<String>) -> Unit = {},
    onMetricPress: (label: String) -> Unit = {},
) {
    val colors = EsuissTheme.colors
    var section by rememberSaveable { mutableStateOf("Summary") }
    var editing by rememberSaveable { mutableStateOf(false) }
    val pinned = remember { mutableStateListOf("Move", "Steps", "Resting Heart Rate", "Sleep") }

    fun open(value: String) {
        section = value
        editing = false
    }

    fun togglePin(label: String) {
        onPinnedChange(if (label in pinned) pinned - label else pinned + label)
        if (label in pinned) pinned.remove(label) else pinned.add(label)
    }

    Screen(modifier, variant = if (colors.isDark) ScreenVariant.Default else ScreenVariant.Secondary) {
        AnimatedContent(
            targetState = section,
            transitionSpec = {
                val forward = targetState != "Summary"
                (fadeIn(tween(300)) + slideInHorizontally(tween(450, easing = HealthEasing)) { if (forward) it / 4 else -it / 4 }) togetherWith
                    fadeOut(tween(150))
            },
            label = "section",
        ) { current ->
            val header = rememberHeaderState()
            val summary = current == "Summary"
            val cards = HealthMetrics.filter { if (summary) editing || it.label in pinned else it.category == current }
            Column(
                Modifier
                    .fillMaxSize()
                    .nestedScroll(header.nestedScrollConnection),
            ) {
                Header(state = header) {
                    if (!summary) HeaderBack(onClick = { open("Summary") }, text = "Summary")
                    HeaderTitle(current)
                    HeaderLargeTitle(current)
                    HeaderActions {
                        Box(
                            Modifier
                                .size(34.dp)
                                .background(Palette.indigo500, CircleShape)
                                .semantics { contentDescription = "Jamie Rivera" },
                            contentAlignment = Alignment.Center,
                        ) {
                            BasicText("JR", style = Typography.xs.copy(color = Palette.white, fontWeight = FontWeight.SemiBold))
                        }
                    }
                }
                Column(
                    Modifier
                        .weight(1f)
                        .fillMaxWidth()
                        .verticalScroll(rememberScrollState())
                        .padding(start = 20.dp, end = 20.dp, top = 8.dp, bottom = 32.dp),
                    verticalArrangement = Arrangement.spacedBy(10.dp),
                ) {
                    Row(verticalAlignment = Alignment.CenterVertically) {
                        BasicText(
                            if (summary) "Pinned" else "Last 7 days",
                            modifier = Modifier
                                .weight(1f)
                                .semantics { heading() },
                            style = Typography.xl.copy(color = colors.foreground, fontWeight = FontWeight.Bold),
                        )
                        if (summary) {
                            Button(onClick = { editing = !editing }, variant = ButtonVariant.Plain, size = ButtonSize.Sm) {
                                BasicText(
                                    if (editing) "Done" else "Edit",
                                    style = Typography.base.copy(color = colors.primary, fontWeight = if (editing) FontWeight.SemiBold else FontWeight.Normal),
                                )
                            }
                        }
                    }
                    if (cards.isEmpty()) {
                        BasicText(
                            "No pinned items. Use Edit to add some.",
                            modifier = Modifier
                                .fillMaxWidth()
                                .padding(vertical = 30.dp),
                            style = Typography.base.copy(color = colors.mutedForeground, textAlign = TextAlign.Center),
                        )
                    }
                    cards.forEachIndexed { index, metric ->
                        key(metric.label) {
                            MetricRow(
                                metric = metric,
                                index = index,
                                editing = editing && summary,
                                pinned = metric.label in pinned,
                                onTogglePin = { togglePin(metric.label) },
                                onClick = { onMetricPress(metric.label) },
                            )
                        }
                    }
                    if (summary) {
                        HealthNav(onOpen = ::open, modifier = Modifier.padding(top = 20.dp))
                    }
                }
            }
        }
    }
}

@Composable
private fun HealthTile(icon: ImageVector, tint: Color) {
    Box(
        Modifier
            .size(28.dp)
            .background(tint, RoundedCornerShape(Radius.md)),
        contentAlignment = Alignment.Center,
    ) {
        Icon(icon, contentDescription = null, size = 16.dp, tint = Palette.white)
    }
}

@Composable
private fun HealthNav(onOpen: (String) -> Unit, modifier: Modifier = Modifier) {
    val colors = EsuissTheme.colors
    val card = if (colors.isDark) colors.muted else colors.background
    Column(modifier, verticalArrangement = Arrangement.spacedBy(20.dp)) {
        HealthNavGroup(card) {
            HealthNavRow("Sharing", PhosphorIcons.Fill.Users, Palette.blue500) { onOpen("Sharing") }
        }
        Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
            BasicText(
                "Categories",
                modifier = Modifier
                    .padding(horizontal = 4.dp)
                    .semantics { heading() },
                style = Typography.xl.copy(color = colors.foreground, fontWeight = FontWeight.Bold),
            )
            HealthNavGroup(card) {
                HealthCategories.forEachIndexed { index, category ->
                    if (index > 0) ItemSeparator(Modifier.padding(start = 40.dp))
                    HealthNavRow(category.value, category.icon, category.tint(colors)) { onOpen(category.value) }
                }
            }
        }
    }
}

@Composable
private fun HealthNavGroup(background: Color, content: @Composable () -> Unit) {
    ItemGroup(
        modifier = Modifier
            .clip(RoundedCornerShape(Radius.xl2))
            .background(background),
        variant = ItemGroupVariant.Inset,
    ) {
        Column(Modifier.background(background)) { content() }
    }
}

@Composable
private fun HealthNavRow(label: String, icon: ImageVector, tint: Color, onClick: () -> Unit) {
    val colors = EsuissTheme.colors
    Item(onClick = onClick) {
        ItemMedia { HealthTile(icon, tint) }
        ItemContent {
            ItemTitle {
                BasicText(label, style = Typography.base.copy(color = colors.foreground))
            }
        }
        ItemActions {
            Icon(PhosphorIcons.Bold.CaretRight, contentDescription = null, size = 12.dp, tint = colors.mutedForeground)
        }
    }
}

@Composable
private fun MetricRow(
    metric: HealthMetric,
    index: Int,
    editing: Boolean,
    pinned: Boolean,
    onTogglePin: () -> Unit,
    onClick: () -> Unit,
) {
    val colors = EsuissTheme.colors
    val category = HealthCategories.first { it.value == metric.category }
    val appear = remember { Animatable(0f) }
    LaunchedEffect(Unit) { appear.animateTo(1f, tween(800, delayMillis = index * 40, easing = HealthEasing)) }
    val dim by animateFloatAsState(if (editing && !pinned) 0.55f else 1f, tween(200), label = "dim")
    val pinFill by animateColorAsState(
        if (pinned) colors.destructive else if (colors.isDark) Palette.green500 else lerp(Palette.green500, colors.foreground, 0.35f),
        tween(200),
        label = "pin",
    )
    val interaction = remember { MutableInteractionSource() }
    val pressed by interaction.collectIsPressedAsState()
    val card = if (colors.isDark) colors.muted else colors.background
    Row(
        modifier = Modifier.graphicsLayer {
            alpha = appear.value
            translationY = (1f - appear.value) * 8.dp.toPx()
        },
        horizontalArrangement = Arrangement.spacedBy(12.dp),
        verticalAlignment = Alignment.CenterVertically,
    ) {
        AnimatedVisibility(visible = editing, enter = fadeIn() + scaleIn(), exit = fadeOut() + scaleOut()) {
            Box(
                Modifier
                    .size(44.dp)
                    .clickable(
                        interactionSource = remember { MutableInteractionSource() },
                        indication = null,
                        role = Role.Button,
                        onClick = onTogglePin,
                    )
                    .semantics { contentDescription = "${if (pinned) "Unpin" else "Pin"} ${metric.label}" },
                contentAlignment = Alignment.Center,
            ) {
                Box(
                    Modifier
                        .size(24.dp)
                        .background(pinFill, CircleShape),
                    contentAlignment = Alignment.Center,
                ) {
                    Icon(if (pinned) PhosphorIcons.Bold.Minus else PhosphorIcons.Bold.Plus, contentDescription = null, size = 12.dp, tint = Palette.white)
                }
            }
        }
        Column(
            modifier = Modifier
                .weight(1f)
                .graphicsLayer { alpha = dim }
                .clip(RoundedCornerShape(Radius.xl))
                .alpha(if (pressed) 0.8f else 1f)
                .background(card)
                .clickable(interactionSource = interaction, indication = null, role = Role.Button, onClick = onClick)
                .padding(horizontal = 16.dp, vertical = 12.dp),
            verticalArrangement = Arrangement.spacedBy(10.dp),
        ) {
            val text = categoryText(colors, category)
            Row(horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically) {
                Icon(category.icon, contentDescription = null, size = 16.dp, tint = text)
                BasicText(
                    metric.label,
                    modifier = Modifier.weight(1f),
                    style = Typography.base.copy(color = text, fontWeight = FontWeight.SemiBold),
                    maxLines = 1,
                )
                BasicText("9:41", style = Typography.sm.copy(color = colors.mutedForeground))
                Icon(PhosphorIcons.Bold.CaretRight, contentDescription = null, size = 12.dp, tint = colors.mutedForeground)
            }
            Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.Bottom) {
                BasicText(
                    buildAnnotatedString {
                        withStyle(SpanStyle(fontSize = Typography.xl2.fontSize, fontWeight = FontWeight.Bold, color = colors.foreground)) {
                            append(metric.value)
                        }
                        if (metric.unit != null) {
                            withStyle(SpanStyle(fontSize = Typography.base.fontSize, fontWeight = FontWeight.SemiBold, color = colors.mutedForeground)) {
                                append(" ${metric.unit}")
                            }
                        }
                    },
                    modifier = Modifier.weight(1f),
                    style = Typography.xl2.copy(fontFeatureSettings = "tnum"),
                )
                Spark(metric.data, metric.color(colors), metric.bars)
            }
        }
    }
}

@Composable
private fun Spark(data: List<Int>, color: Color, bars: Boolean) {
    val max = data.max()
    val min = data.min()
    if (bars) {
        Row(
            modifier = Modifier
                .height(36.dp)
                .clearAndSetSemantics {},
            horizontalArrangement = Arrangement.spacedBy(4.dp),
            verticalAlignment = Alignment.Bottom,
        ) {
            data.forEachIndexed { index, value ->
                Box(
                    Modifier
                        .width(8.dp)
                        .height(36.dp * (value.toFloat() / max))
                        .graphicsLayer { alpha = if (index == data.lastIndex) 1f else 0.45f }
                        .background(color, RoundedCornerShape(3.dp)),
                )
            }
        }
        return
    }
    Canvas(
        Modifier
            .size(width = 120.dp, height = 36.dp)
            .clearAndSetSemantics {},
    ) {
        val range = (max - min).coerceAtLeast(1).toFloat()
        val path = Path()
        data.forEachIndexed { index, value ->
            val point = Offset(
                size.width * index / (data.size - 1),
                size.height - ((value - min) / range) * (size.height * 30f / 36f) - size.height * 3f / 36f,
            )
            if (index == 0) path.moveTo(point.x, point.y) else path.lineTo(point.x, point.y)
        }
        drawPath(path, color, style = Stroke(width = 2.2.dp.toPx(), cap = StrokeCap.Round, join = StrokeJoin.Round))
    }
}
```

Composes `Screen`, `Header` with a back button, `Avatar`, `Button`, `Icon` and `Item` groups, with sparklines drawn using `react-native-svg`. The list opens a section of metric cards, and the Summary section has an Edit mode for pinning and unpinning cards.

## Installation

**React Native**

```bash title="Terminal"
npx shadcn@latest add @esuiss-rn/dash-health
```

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 dash-health
```

**SwiftUI**

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

**Jetpack Compose**

```bash title="Terminal"
npx @esuiss/ui@latest add dash-health --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), [Item](https://ui.suiss.app/docs/components/item), [Screen](https://ui.suiss.app/docs/components/screen), [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), [Item](https://ui.suiss.app/docs/components/item), [Screen](https://ui.suiss.app/docs/components/screen), [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/dash-health.tsx`:

- `uniwind/components/blocks/dash-health.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), [Item](https://ui.suiss.app/docs/components/item), [Screen](https://ui.suiss.app/docs/components/screen), [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/dash-health.tsx`:

```tsx title="nativewind/components/blocks/dash-health.tsx"
import { cn } from "cn"
import {
  CaretRightIcon,
  ChartBarIcon,
  FlameIcon,
  HeartIcon,
  MinusIcon,
  MoonIcon,
  type Icon as PhosphorIcon,
  type IconProps as PhosphorIconProps,
  PlusIcon,
  UsersIcon,
} from "phosphor-react-native"
import * as React from "react"
import { I18nManager, Pressable, View } from "react-native"
import { FadeInDown, ReduceMotion } from "react-native-reanimated"
import Svg, { Polyline } from "react-native-svg"
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 {
  Item,
  ItemActions,
  ItemContent,
  ItemGroup,
  ItemMedia,
  ItemSeparator,
  ItemTitle,
} from "@/components/ui/item"
import { Screen } from "@/components/ui/screen"
import { Text } from "@/components/ui/text"

type Category = "Activity" | "Heart" | "Sleep" | "Mobility"

type Section = "Summary" | "Sharing" | Category

type CategoryStyle = {
  value: Category
  icon: PhosphorIcon
  tint: string
  text: string
}

const CATEGORIES: [CategoryStyle, ...CategoryStyle[]] = [
  {
    value: "Activity",
    icon: FlameIcon,
    tint: "bg-orange-500",
    text: "text-orange-500",
  },
  { value: "Heart", icon: HeartIcon, tint: "bg-red-500", text: "text-red-500" },
  {
    value: "Sleep",
    icon: MoonIcon,
    tint: "bg-cyan-500",
    text: "text-cyan-500",
  },
  {
    value: "Mobility",
    icon: ChartBarIcon,
    tint: "bg-green-500",
    text: "text-green-500",
  },
]

const SECTIONS: { value: Section; icon: PhosphorIcon; tint: string }[] = [
  { value: "Summary", icon: HeartIcon, tint: "bg-pink-500" },
  { value: "Sharing", icon: UsersIcon, tint: "bg-blue-500" },
]

type Metric = {
  category: Category
  label: string
  value: string
  unit?: string
  bar: string
  line: string
  data: number[]
  bars?: boolean
}

const METRICS: Metric[] = [
  {
    category: "Activity",
    label: "Move",
    value: "486",
    unit: "kcal",
    bar: "bg-red-500",
    line: "text-red-500",
    data: [40, 62, 38, 70, 55, 81, 65],
    bars: true,
  },
  {
    category: "Activity",
    label: "Steps",
    value: "8,412",
    unit: "steps",
    bar: "bg-orange-500",
    line: "text-orange-500",
    data: [60, 80, 45, 90, 70, 95, 52],
    bars: true,
  },
  {
    category: "Heart",
    label: "Resting Heart Rate",
    value: "58",
    unit: "bpm",
    bar: "bg-red-500",
    line: "text-red-500",
    data: [61, 60, 59, 58, 60, 57, 58],
  },
  {
    category: "Sleep",
    label: "Sleep",
    value: "7 hr 12 min",
    bar: "bg-cyan-500",
    line: "text-cyan-500",
    data: [68, 74, 59, 79, 64, 86, 72],
    bars: true,
  },
  {
    category: "Heart",
    label: "Cardio Fitness",
    value: "42.1",
    unit: "VO₂ max",
    bar: "bg-red-500",
    line: "text-red-500",
    data: [40, 40, 41, 41, 42, 42, 42],
  },
  {
    category: "Mobility",
    label: "Walking Speed",
    value: "5.1",
    unit: "km/h",
    bar: "bg-green-500",
    line: "text-green-500",
    data: [48, 50, 49, 51, 50, 52, 51],
  },
]

const DAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]

const SparkContext = React.createContext("")

function SparkLine({ color }: PhosphorIconProps) {
  const points = React.useContext(SparkContext)
  return (
    <Svg width={120} height={36} viewBox="0 0 120 36">
      <Polyline
        points={points}
        fill="none"
        stroke={color}
        strokeWidth={2.2}
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </Svg>
  )
}

function Spark({ metric }: { metric: Metric }) {
  const max = Math.max(...metric.data)
  const min = Math.min(...metric.data)

  if (metric.bars) {
    return (
      <View
        accessibilityElementsHidden
        importantForAccessibility="no-hide-descendants"
        className="h-9 flex-row items-end gap-1"
      >
        {metric.data.map((value, index) => (
          <View
            key={DAYS[index]}
            style={{ height: `${(value / max) * 100}%` }}
            className={cn(
              "w-2 rounded-[3px]",
              metric.bar,
              index < metric.data.length - 1 && "opacity-45"
            )}
          />
        ))}
      </View>
    )
  }

  const points = metric.data
    .map(
      (value, index) =>
        `${((index / (metric.data.length - 1)) * 120).toFixed(1)},${(36 - ((value - min) / Math.max(1, max - min)) * 30 - 3).toFixed(1)}`
    )
    .join(" ")

  return (
    <View
      accessibilityElementsHidden
      importantForAccessibility="no-hide-descendants"
    >
      <SparkContext.Provider value={points}>
        <Icon as={SparkLine} className={cn("size-9", metric.line)} />
      </SparkContext.Provider>
    </View>
  )
}

function Tile({ icon, tint }: { icon: PhosphorIcon; tint: string }) {
  return (
    <ItemMedia className={cn("size-[1.8125rem] rounded-[7px]", tint)}>
      <Icon
        as={icon}
        weight="fill"
        className="size-4 text-primary-foreground"
      />
    </ItemMedia>
  )
}

function Chevron({ className }: { className?: string }) {
  return (
    <Icon
      as={CaretRightIcon}
      weight="bold"
      className={cn(
        "size-3.5 text-muted-foreground",
        I18nManager.isRTL && "rotate-180",
        className
      )}
    />
  )
}

function MetricCard({
  metric,
  index,
  editing,
  pinned,
  onTogglePin,
  onPress,
}: {
  metric: Metric
  index: number
  editing: boolean
  pinned: boolean
  onTogglePin: () => void
  onPress: () => void
}) {
  const category =
    CATEGORIES.find((item) => item.value === metric.category) ?? CATEGORIES[0]
  return (
    <AnimatedView
      entering={FadeInDown.delay(index * 40)
        .duration(800)
        .reduceMotion(ReduceMotion.System)}
      className="flex-row items-center gap-3"
    >
      {editing ? (
        <Button
          size="icon-xs"
          accessibilityLabel={`${pinned ? "Unpin" : "Pin"} ${metric.label}`}
          hitSlop={10}
          onPress={onTogglePin}
          className={pinned ? "bg-destructive" : "bg-green-500"}
        >
          <Icon
            as={pinned ? MinusIcon : PlusIcon}
            weight="bold"
            className="size-3.5 text-primary-foreground"
          />
        </Button>
      ) : null}
      <Pressable
        role="button"
        onPress={onPress}
        className={cn(
          "min-w-0 flex-1 gap-2.5 rounded-xl bg-background px-4 py-3 active:opacity-80 dark:bg-muted",
          editing && !pinned && "opacity-55"
        )}
      >
        <View className="flex-row items-center gap-1.5">
          <Icon
            as={category.icon}
            weight="fill"
            className={cn("size-4", category.text)}
          />
          <Text
            numberOfLines={1}
            className={cn("flex-1 text-base font-semibold", category.text)}
          >
            {metric.label}
          </Text>
          <Text className="text-sm text-muted-foreground">9:41</Text>
          <Chevron className="size-3" />
        </View>
        <View className="flex-row items-end gap-4">
          <Text className="flex-1">
            <Text className="text-3xl font-bold tabular-nums">
              {metric.value}
            </Text>
            {metric.unit ? (
              <Text className="text-base font-semibold text-muted-foreground">
                {` ${metric.unit}`}
              </Text>
            ) : null}
          </Text>
          <Spark metric={metric} />
        </View>
      </Pressable>
    </AnimatedView>
  )
}

function SectionView({
  section,
  onMetricPress,
  onPinnedChange,
}: {
  section: Section
  onMetricPress?: (label: string) => void
  onPinnedChange?: (pinned: string[]) => void
}) {
  const [editing, setEditing] = React.useState(false)
  const [pinned, setPinned] = React.useState<string[]>([
    "Move",
    "Steps",
    "Resting Heart Rate",
    "Sleep",
  ])
  const summary = section === "Summary"
  const cards = METRICS.filter((metric) =>
    summary
      ? editing || pinned.includes(metric.label)
      : metric.category === section
  )

  const togglePin = (label: string) => {
    const next = pinned.includes(label)
      ? pinned.filter((item) => item !== label)
      : [...pinned, label]
    setPinned(next)
    onPinnedChange?.(next)
  }

  return (
    <View className="gap-2.5 px-4">
      <View className="mt-2.5 flex-row items-center gap-2">
        <Text role="heading" className="flex-1 text-xl font-bold">
          {summary ? "Pinned" : "Last 7 days"}
        </Text>
        {summary ? (
          <Button
            variant="plain"
            size="sm"
            accessibilityState={{ selected: editing }}
            onPress={() => setEditing(!editing)}
          >
            <Text className={cn("text-base", editing && "font-semibold")}>
              {editing ? "Done" : "Edit"}
            </Text>
          </Button>
        ) : null}
      </View>
      {cards.length > 0 ? (
        <View className="gap-2.5">
          {cards.map((metric, index) => (
            <MetricCard
              key={metric.label}
              metric={metric}
              index={index}
              editing={editing}
              pinned={pinned.includes(metric.label)}
              onTogglePin={() => togglePin(metric.label)}
              onPress={() => onMetricPress?.(metric.label)}
            />
          ))}
        </View>
      ) : (
        <Text className="py-[1.875rem] text-center text-base text-muted-foreground">
          No pinned items. Use Edit to add some.
        </Text>
      )}
    </View>
  )
}

function ProfileAvatar() {
  return (
    <Avatar className="size-[2.125rem]">
      <AvatarFallback className="bg-indigo-500">JR</AvatarFallback>
    </Avatar>
  )
}

function SectionRow({
  label,
  icon,
  tint,
  onPress,
}: {
  label: string
  icon: PhosphorIcon
  tint: string
  onPress: () => void
}) {
  return (
    <Item size="sm" onPress={onPress} className="min-h-11 gap-3.5 py-2">
      <Tile icon={icon} tint={tint} />
      <ItemContent>
        <ItemTitle className="font-normal">{label}</ItemTitle>
      </ItemContent>
      <ItemActions>
        <Chevron />
      </ItemActions>
    </Item>
  )
}

function DashHealth({
  onMetricPress,
  onPinnedChange,
}: {
  onMetricPress?: (label: string) => void
  onPinnedChange?: (pinned: string[]) => void
}) {
  const [section, setSection] = React.useState<Section | null>("Summary")
  const scroll = useHeaderScroll()

  return (
    <Screen
      variant="secondary"
      edges={["top", "right", "left"]}
      className="dark:bg-background"
    >
      <Header scroll={scroll}>
        {section ? (
          <HeaderBack onPress={() => setSection(null)}>Health</HeaderBack>
        ) : null}
        <HeaderTitle>{section ?? "Health"}</HeaderTitle>
      </Header>
      <HeaderScrollView key={section ?? "health"} scroll={scroll}>
        <HeaderLargeTitle className="flex-row items-center gap-2.5">
          <Text role="heading" className="flex-1 text-3xl font-bold">
            {section ?? "Health"}
          </Text>
          <ProfileAvatar />
        </HeaderLargeTitle>
        {section ? (
          <SectionView
            section={section}
            onMetricPress={onMetricPress}
            onPinnedChange={onPinnedChange}
          />
        ) : (
          <View className="gap-2 px-4">
            <ItemGroup variant="inset" className="bg-background dark:bg-muted">
              {SECTIONS.map((item, index) => (
                <React.Fragment key={item.value}>
                  {index > 0 ? (
                    <ItemSeparator className="ms-[3.5625rem]" />
                  ) : null}
                  <SectionRow
                    label={item.value}
                    icon={item.icon}
                    tint={item.tint}
                    onPress={() => setSection(item.value)}
                  />
                </React.Fragment>
              ))}
            </ItemGroup>
            <Text
              role="heading"
              className="mt-4 px-1 text-xl font-bold text-foreground"
            >
              Categories
            </Text>
            <ItemGroup variant="inset" className="bg-background dark:bg-muted">
              {CATEGORIES.map((item, index) => (
                <React.Fragment key={item.value}>
                  {index > 0 ? (
                    <ItemSeparator className="ms-[3.5625rem]" />
                  ) : null}
                  <SectionRow
                    label={item.value}
                    icon={item.icon}
                    tint={item.tint}
                    onPress={() => setSection(item.value)}
                  />
                </React.Fragment>
              ))}
            </ItemGroup>
          </View>
        )}
        <View className="h-8" />
      </HeaderScrollView>
    </Screen>
  )
}

export { DashHealth }
```

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), [Item](https://ui.suiss.app/docs/components/item), [Screen](https://ui.suiss.app/docs/components/screen), [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/dash-health.tsx`:

```tsx title="stylesheet/components/blocks/dash-health.tsx"
import {
  CaretRightIcon,
  ChartBarIcon,
  FlameIcon,
  HeartIcon,
  MinusIcon,
  MoonIcon,
  type Icon as PhosphorIcon,
  PlusIcon,
  UsersIcon,
} from "phosphor-react-native"
import * as React from "react"
import { I18nManager, Pressable, StyleSheet, View } from "react-native"
import { FadeInDown, ReduceMotion } from "react-native-reanimated"
import Svg, { Polyline } from "react-native-svg"
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 {
  Item,
  ItemActions,
  ItemContent,
  ItemGroup,
  ItemMedia,
  ItemSeparator,
  ItemTitle,
} from "@/components/ui/item"
import { Screen } from "@/components/ui/screen"
import { Text } from "@/components/ui/text"
import {
  fontSize,
  palette,
  radius,
  type ThemeColors,
  useTheme,
} from "@/components/ui/theme"

type Tone = keyof ThemeColors | keyof typeof palette

function tone(colors: ThemeColors, key: Tone) {
  return { ...colors, ...palette }[key]
}

type Tint = Tone

type Category = "Activity" | "Heart" | "Sleep" | "Mobility"

type Section = "Summary" | "Sharing" | Category

type CategoryStyle = {
  value: Category
  icon: PhosphorIcon
  tint: Tint
  text: Tint
}

const CATEGORIES: [CategoryStyle, ...CategoryStyle[]] = [
  {
    value: "Activity",
    icon: FlameIcon,
    tint: "orange500",
    text: "orange500",
  },
  { value: "Heart", icon: HeartIcon, tint: "red500", text: "red500" },
  { value: "Sleep", icon: MoonIcon, tint: "cyan500", text: "cyan500" },
  {
    value: "Mobility",
    icon: ChartBarIcon,
    tint: "green500",
    text: "green500",
  },
]

const SECTIONS: { value: Section; icon: PhosphorIcon; tint: Tint }[] = [
  { value: "Summary", icon: HeartIcon, tint: "pink500" },
  { value: "Sharing", icon: UsersIcon, tint: "blue500" },
]

type Metric = {
  category: Category
  label: string
  value: string
  unit?: string
  bar: Tint
  line: Tint
  data: number[]
  bars?: boolean
}

const METRICS: Metric[] = [
  {
    category: "Activity",
    label: "Move",
    value: "486",
    unit: "kcal",
    bar: "red500",
    line: "red500",
    data: [40, 62, 38, 70, 55, 81, 65],
    bars: true,
  },
  {
    category: "Activity",
    label: "Steps",
    value: "8,412",
    unit: "steps",
    bar: "orange500",
    line: "orange500",
    data: [60, 80, 45, 90, 70, 95, 52],
    bars: true,
  },
  {
    category: "Heart",
    label: "Resting Heart Rate",
    value: "58",
    unit: "bpm",
    bar: "red500",
    line: "red500",
    data: [61, 60, 59, 58, 60, 57, 58],
  },
  {
    category: "Sleep",
    label: "Sleep",
    value: "7 hr 12 min",
    bar: "cyan500",
    line: "cyan500",
    data: [68, 74, 59, 79, 64, 86, 72],
    bars: true,
  },
  {
    category: "Heart",
    label: "Cardio Fitness",
    value: "42.1",
    unit: "VO₂ max",
    bar: "red500",
    line: "red500",
    data: [40, 40, 41, 41, 42, 42, 42],
  },
  {
    category: "Mobility",
    label: "Walking Speed",
    value: "5.1",
    unit: "km/h",
    bar: "green500",
    line: "green500",
    data: [48, 50, 49, 51, 50, 52, 51],
  },
]

const DAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]

const styles = StyleSheet.create({
  bars: {
    height: 36,
    flexDirection: "row",
    alignItems: "flex-end",
    gap: 4,
  },
  bar: { width: 8, borderRadius: 3 },
  tile: { width: 29, height: 29, borderRadius: 7 },
  card: { flexDirection: "row", alignItems: "center", gap: 12 },
  cardBody: {
    minWidth: 0,
    flex: 1,
    gap: 10,
    borderRadius: radius.xl,
    paddingHorizontal: 16,
    paddingVertical: 12,
  },
  cardTop: { flexDirection: "row", alignItems: "center", gap: 6 },
  cardLabel: { ...fontSize.base, flex: 1, fontWeight: "600" },
  cardBottom: { flexDirection: "row", alignItems: "flex-end", gap: 16 },
  value: {
    ...fontSize["3xl"],
    fontWeight: "700",
    fontVariant: ["tabular-nums"],
  },
  unit: { ...fontSize.base, fontWeight: "600" },
  section: { gap: 10, paddingHorizontal: 16 },
  sectionTop: {
    marginTop: 10,
    flexDirection: "row",
    alignItems: "center",
    gap: 8,
  },
  sectionTitle: { ...fontSize.xl, flex: 1, fontWeight: "700" },
  cards: { gap: 10 },
  empty: { ...fontSize.base, paddingVertical: 30, textAlign: "center" },
  row: { minHeight: 44, gap: 14, paddingVertical: 8 },
  largeTitle: { flexDirection: "row", alignItems: "center", gap: 10 },
  largeTitleText: { ...fontSize["3xl"], flex: 1, fontWeight: "700" },
  home: { gap: 8, paddingHorizontal: 16 },
  categories: {
    ...fontSize.xl,
    marginTop: 16,
    paddingHorizontal: 4,
    fontWeight: "700",
  },
})

function Spark({ metric }: { metric: Metric }) {
  const { colors } = useTheme()
  const max = Math.max(...metric.data)
  const min = Math.min(...metric.data)

  if (metric.bars) {
    return (
      <View
        accessibilityElementsHidden
        importantForAccessibility="no-hide-descendants"
        style={styles.bars}
      >
        {metric.data.map((value, index) => (
          <View
            key={DAYS[index]}
            style={[
              styles.bar,
              {
                height: `${(value / max) * 100}%`,
                backgroundColor: tone(colors, metric.bar),
              },
              index < metric.data.length - 1 && { opacity: 0.45 },
            ]}
          />
        ))}
      </View>
    )
  }

  const points = metric.data
    .map(
      (value, index) =>
        `${((index / (metric.data.length - 1)) * 120).toFixed(1)},${(36 - ((value - min) / Math.max(1, max - min)) * 30 - 3).toFixed(1)}`
    )
    .join(" ")

  return (
    <View
      accessibilityElementsHidden
      importantForAccessibility="no-hide-descendants"
    >
      <Svg width={120} height={36} viewBox="0 0 120 36">
        <Polyline
          points={points}
          fill="none"
          stroke={tone(colors, metric.line)}
          strokeWidth={2.2}
          strokeLinecap="round"
          strokeLinejoin="round"
        />
      </Svg>
    </View>
  )
}

function Tile({ icon, tint }: { icon: PhosphorIcon; tint: Tint }) {
  const { colors } = useTheme()
  return (
    <ItemMedia style={[styles.tile, { backgroundColor: tone(colors, tint) }]}>
      <Icon
        as={icon}
        weight="fill"
        size={16}
        color={colors.primaryForeground}
      />
    </ItemMedia>
  )
}

function Chevron({ size = 14 }: { size?: number }) {
  const { colors } = useTheme()
  return (
    <Icon
      as={CaretRightIcon}
      weight="bold"
      size={size}
      color={colors.mutedForeground}
      style={
        I18nManager.isRTL ? { transform: [{ rotate: "180deg" }] } : undefined
      }
    />
  )
}

function MetricCard({
  metric,
  index,
  editing,
  pinned,
  onTogglePin,
  onPress,
}: {
  metric: Metric
  index: number
  editing: boolean
  pinned: boolean
  onTogglePin: () => void
  onPress: () => void
}) {
  const { colors, isDark } = useTheme()
  const category =
    CATEGORIES.find((item) => item.value === metric.category) ?? CATEGORIES[0]
  return (
    <AnimatedView
      entering={FadeInDown.delay(index * 40)
        .duration(800)
        .reduceMotion(ReduceMotion.System)}
      style={styles.card}
    >
      {editing ? (
        <Button
          size="icon-xs"
          accessibilityLabel={`${pinned ? "Unpin" : "Pin"} ${metric.label}`}
          hitSlop={10}
          onPress={onTogglePin}
          style={{
            backgroundColor: pinned ? colors.destructive : palette.green500,
          }}
        >
          <Icon
            as={pinned ? MinusIcon : PlusIcon}
            weight="bold"
            size={14}
            color={colors.primaryForeground}
          />
        </Button>
      ) : null}
      <Pressable
        role="button"
        onPress={onPress}
        style={({ pressed }) => [
          styles.cardBody,
          {
            backgroundColor: isDark ? colors.muted : colors.background,
          },
          editing && !pinned && { opacity: 0.55 },
          pressed && { opacity: 0.8 },
        ]}
      >
        <View style={styles.cardTop}>
          <Icon
            as={category.icon}
            weight="fill"
            size={16}
            color={tone(colors, category.text)}
          />
          <Text
            numberOfLines={1}
            style={[styles.cardLabel, { color: tone(colors, category.text) }]}
          >
            {metric.label}
          </Text>
          <Text style={[fontSize.sm, { color: colors.mutedForeground }]}>
            9:41
          </Text>
          <Chevron size={12} />
        </View>
        <View style={styles.cardBottom}>
          <Text style={{ flex: 1 }}>
            <Text style={styles.value}>{metric.value}</Text>
            {metric.unit ? (
              <Text style={[styles.unit, { color: colors.mutedForeground }]}>
                {` ${metric.unit}`}
              </Text>
            ) : null}
          </Text>
          <Spark metric={metric} />
        </View>
      </Pressable>
    </AnimatedView>
  )
}

function SectionView({
  section,
  onMetricPress,
  onPinnedChange,
}: {
  section: Section
  onMetricPress?: (label: string) => void
  onPinnedChange?: (pinned: string[]) => void
}) {
  const { colors } = useTheme()
  const [editing, setEditing] = React.useState(false)
  const [pinned, setPinned] = React.useState<string[]>([
    "Move",
    "Steps",
    "Resting Heart Rate",
    "Sleep",
  ])
  const summary = section === "Summary"
  const cards = METRICS.filter((metric) =>
    summary
      ? editing || pinned.includes(metric.label)
      : metric.category === section
  )

  const togglePin = (label: string) => {
    const next = pinned.includes(label)
      ? pinned.filter((item) => item !== label)
      : [...pinned, label]
    setPinned(next)
    onPinnedChange?.(next)
  }

  return (
    <View style={styles.section}>
      <View style={styles.sectionTop}>
        <Text role="heading" style={styles.sectionTitle}>
          {summary ? "Pinned" : "Last 7 days"}
        </Text>
        {summary ? (
          <Button
            variant="plain"
            size="sm"
            accessibilityState={{ selected: editing }}
            onPress={() => setEditing(!editing)}
          >
            <Text style={[fontSize.base, editing && { fontWeight: "600" }]}>
              {editing ? "Done" : "Edit"}
            </Text>
          </Button>
        ) : null}
      </View>
      {cards.length > 0 ? (
        <View style={styles.cards}>
          {cards.map((metric, index) => (
            <MetricCard
              key={metric.label}
              metric={metric}
              index={index}
              editing={editing}
              pinned={pinned.includes(metric.label)}
              onTogglePin={() => togglePin(metric.label)}
              onPress={() => onMetricPress?.(metric.label)}
            />
          ))}
        </View>
      ) : (
        <Text style={[styles.empty, { color: colors.mutedForeground }]}>
          No pinned items. Use Edit to add some.
        </Text>
      )}
    </View>
  )
}

function ProfileAvatar() {
  return (
    <Avatar style={{ width: 34, height: 34 }}>
      <AvatarFallback style={{ backgroundColor: palette.indigo500 }}>
        JR
      </AvatarFallback>
    </Avatar>
  )
}

function SectionRow({
  label,
  icon,
  tint,
  onPress,
}: {
  label: string
  icon: PhosphorIcon
  tint: Tint
  onPress: () => void
}) {
  return (
    <Item size="sm" onPress={onPress} style={styles.row}>
      <Tile icon={icon} tint={tint} />
      <ItemContent>
        <ItemTitle style={{ fontWeight: "400" }}>{label}</ItemTitle>
      </ItemContent>
      <ItemActions>
        <Chevron />
      </ItemActions>
    </Item>
  )
}

function DashHealth({
  onMetricPress,
  onPinnedChange,
}: {
  onMetricPress?: (label: string) => void
  onPinnedChange?: (pinned: string[]) => void
}) {
  const { colors, isDark } = useTheme()
  const [section, setSection] = React.useState<Section | null>("Summary")
  const scroll = useHeaderScroll()

  const group = {
    backgroundColor: isDark ? colors.muted : colors.background,
  }

  return (
    <Screen
      variant="secondary"
      edges={["top", "right", "left"]}
      style={isDark ? { backgroundColor: colors.background } : undefined}
    >
      <Header scroll={scroll}>
        {section ? (
          <HeaderBack onPress={() => setSection(null)}>Health</HeaderBack>
        ) : null}
        <HeaderTitle>{section ?? "Health"}</HeaderTitle>
      </Header>
      <HeaderScrollView key={section ?? "health"} scroll={scroll}>
        <HeaderLargeTitle style={styles.largeTitle}>
          <Text role="heading" style={styles.largeTitleText}>
            {section ?? "Health"}
          </Text>
          <ProfileAvatar />
        </HeaderLargeTitle>
        {section ? (
          <SectionView
            section={section}
            onMetricPress={onMetricPress}
            onPinnedChange={onPinnedChange}
          />
        ) : (
          <View style={styles.home}>
            <ItemGroup variant="inset" style={group}>
              {SECTIONS.map((item, index) => (
                <React.Fragment key={item.value}>
                  {index > 0 ? (
                    <ItemSeparator style={{ marginStart: 57 }} />
                  ) : null}
                  <SectionRow
                    label={item.value}
                    icon={item.icon}
                    tint={item.tint}
                    onPress={() => setSection(item.value)}
                  />
                </React.Fragment>
              ))}
            </ItemGroup>
            <Text
              role="heading"
              style={[styles.categories, { color: colors.foreground }]}
            >
              Categories
            </Text>
            <ItemGroup variant="inset" style={group}>
              {CATEGORIES.map((item, index) => (
                <React.Fragment key={item.value}>
                  {index > 0 ? (
                    <ItemSeparator style={{ marginStart: 57 }} />
                  ) : null}
                  <SectionRow
                    label={item.value}
                    icon={item.icon}
                    tint={item.tint}
                    onPress={() => setSection(item.value)}
                  />
                </React.Fragment>
              ))}
            </ItemGroup>
          </View>
        )}
        <View style={{ height: 32 }} />
      </HeaderScrollView>
    </Screen>
  )
}

export { DashHealth }
```

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

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

- `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/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/Item.kt`: see [Item](https://ui.suiss.app/docs/components/item.md)

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

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

## Usage

```tsx
import { DashHealth } from "@/components/blocks/dash-health"
```

```tsx
<DashHealth />
```

Replace `METRICS` and `CATEGORIES` with your own readings. Pass `onMetricPress` to open a metric and `onPinnedChange` to store the pinned list.

