# iOS Clock

> World clock, alarms, stopwatch and timer.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/templates/ios-clock
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/ios-clock.json
```

Adds the page at `app/ios-clock/page.tsx` together with the components below. Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Components

- [iOS Kit](https://chromotion.dev/r/ios-kit.md): `@/components/ios-kit`

## Dependencies

```bash
npm install lucide-react
```

## Source

```tsx title="app/ios-clock/page.tsx"
"use client"

import * as React from "react"
import { AlarmClock, Globe, Plus, Timer as TimerIcon, Watch } from "lucide-react"

import { cn } from "@/lib/utils"
import {
  IOSDevice,
  IOSGroup,
  IOSNavButton,
  IOSPage,
  IOSSheet,
  IOSSwitch,
  IOSTabBar,
  type IOSTabItem,
} from "@/components/ios-kit"

const tabs: IOSTabItem[] = [
  { id: "world", label: "World Clock", icon: Globe },
  { id: "alarm", label: "Alarm", icon: AlarmClock },
  { id: "stopwatch", label: "Stopwatch", icon: Watch },
  { id: "timer", label: "Timer", icon: TimerIcon },
]

const cities = [
  { city: "Cupertino", note: "Today, +0HRS", time: "9:41", period: "AM" },
  { city: "New York", note: "Today, +3HRS", time: "12:41", period: "PM" },
  { city: "London", note: "Today, +8HRS", time: "5:41", period: "PM" },
  { city: "Tokyo", note: "Tomorrow, +17HRS", time: "2:41", period: "AM" },
]

const initialAlarms = [
  { id: 1, time: "6:30", period: "AM", label: "Weekdays", on: true },
  { id: 2, time: "7:45", period: "AM", label: "Gym", on: false },
  { id: 3, time: "9:00", period: "AM", label: "Weekend", on: true },
]

const format = (ms: number) => {
  const cs = Math.floor(ms / 10)
  const m = String(Math.floor(cs / 6000)).padStart(2, "0")
  const s = String(Math.floor((cs % 6000) / 100)).padStart(2, "0")
  return `${m}:${s}.${String(cs % 100).padStart(2, "0")}`
}

function Round({ children, className, ...props }: React.ComponentProps<"button">) {
  return (
    <button
      type="button"
      className={cn("flex size-[84px] items-center justify-center rounded-full text-[17px] font-medium transition-opacity active:opacity-60 disabled:opacity-40", className)}
      {...props}
    >
      {children}
    </button>
  )
}

function Stopwatch() {
  const [ms, setMs] = React.useState(0)
  const [laps, setLaps] = React.useState<number[]>([])
  const [running, setRunning] = React.useState(false)

  React.useEffect(() => {
    if (!running) return
    const id = setInterval(() => setMs((value) => value + 50), 50)
    return () => clearInterval(id)
  }, [running])

  return (
    <div className="flex flex-col items-center">
      <p className="mt-6 text-[76px] leading-[84px] font-thin tabular-nums" aria-label="Elapsed time">{format(ms)}</p>
      <div className="mt-8 flex w-full justify-between px-8">
        <Round
          className="bg-(color:--ios-fill)"
          disabled={!running && ms === 0}
          onClick={() => (running ? setLaps((l) => [ms, ...l]) : (setMs(0), setLaps([])))}
        >
          {running ? "Lap" : "Reset"}
        </Round>
        <Round
          className={running ? "bg-(color:--ios-red)/20 text-(color:--ios-red)" : "bg-(color:--ios-green)/20 text-(color:--ios-green)"}
          onClick={() => setRunning((value) => !value)}
        >
          {running ? "Stop" : "Start"}
        </Round>
      </div>
      <ol className="mt-6 w-full px-5">
        {laps.map((lap, i) => (
          <li key={laps.length - i} className="flex justify-between border-t border-(color:--ios-sep) py-2.5 text-[17px] tabular-nums">
            <span>Lap {laps.length - i}</span>
            <span>{format(lap)}</span>
          </li>
        ))}
      </ol>
    </div>
  )
}

function Timer() {
  const TOTAL = 300
  const [left, setLeft] = React.useState(TOTAL)
  const [running, setRunning] = React.useState(false)

  React.useEffect(() => {
    if (!running) return
    const id = setInterval(() => setLeft((value) => Math.max(0, value - 1)), 1000)
    return () => clearInterval(id)
  }, [running])

  const done = left === 0
  const radius = 130
  const circumference = 2 * Math.PI * radius

  return (
    <div className="flex flex-col items-center">
      <div className="relative mt-8 flex size-[290px] items-center justify-center">
        <svg viewBox="0 0 290 290" className="absolute inset-0 -rotate-90" aria-hidden>
          <circle cx="145" cy="145" r={radius} fill="none" strokeWidth="8" className="stroke-(color:--ios-fill)" />
          <circle
            cx="145" cy="145" r={radius} fill="none" strokeWidth="8" strokeLinecap="round"
            className="stroke-(color:--ios-orange) transition-[stroke-dashoffset] duration-1000 ease-linear"
            strokeDasharray={circumference}
            strokeDashoffset={circumference * (1 - left / TOTAL)}
          />
        </svg>
        <p className="text-[72px] font-thin tabular-nums" aria-live="off">
          {Math.floor(left / 60)}:{String(left % 60).padStart(2, "0")}
        </p>
      </div>
      <div className="mt-10 flex w-full justify-between px-8">
        <Round className="bg-(color:--ios-fill)" onClick={() => (setRunning(false), setLeft(TOTAL))}>Cancel</Round>
        <Round
          className={running ? "bg-(color:--ios-orange)/20 text-(color:--ios-orange)" : "bg-(color:--ios-green)/20 text-(color:--ios-green)"}
          disabled={done}
          onClick={() => setRunning((value) => !value)}
        >
          {running ? "Pause" : "Start"}
        </Round>
      </div>
    </div>
  )
}

export default function IOSClock() {
  const [tab, setTab] = React.useState("alarm")
  const [alarms, setAlarms] = React.useState(initialAlarms)
  const [adding, setAdding] = React.useState(false)
  const [hour, setHour] = React.useState("7")

  const titles: Record<string, string> = { world: "World Clock", alarm: "Alarm", stopwatch: "Stopwatch", timer: "Timer" }

  return (
    <IOSDevice>
      <IOSPage
        title={titles[tab]}
        large={tab === "world" || tab === "alarm"}
        tabbed
        left={tab === "world" || tab === "alarm" ? <IOSNavButton>Edit</IOSNavButton> : undefined}
        right={
          tab === "alarm" ? (
            <IOSNavButton aria-label="Add alarm" onClick={() => setAdding(true)}><Plus className="size-7" /></IOSNavButton>
          ) : tab === "world" ? (
            <IOSNavButton aria-label="Add city"><Plus className="size-7" /></IOSNavButton>
          ) : undefined
        }
      >
        {tab === "world" && (
          <ul className="px-4">
            {cities.map((c) => (
              <li key={c.city} className="flex items-center justify-between border-b border-(color:--ios-sep) py-3">
                <span>
                  <span className="block text-[13px] text-(color:--ios-label2)">{c.note}</span>
                  <span className="block text-[24px] leading-8">{c.city}</span>
                </span>
                <span className="text-[48px] leading-[52px] font-light tabular-nums">
                  {c.time}
                  <span className="ml-1 text-[22px]">{c.period}</span>
                </span>
              </li>
            ))}
          </ul>
        )}

        {tab === "alarm" && (
          <IOSGroup header="Other">
            {alarms.map((alarm) => (
              <div key={alarm.id} className="relative flex items-center justify-between px-4 py-2.5 after:absolute after:right-0 after:bottom-0 after:left-4 after:h-px after:bg-(color:--ios-sep) last:after:hidden">
                <span className={cn(!alarm.on && "text-(color:--ios-label3)")}>
                  <span className="text-[48px] leading-[52px] font-light tabular-nums">
                    {alarm.time}
                    <span className="ml-1 text-[22px]">{alarm.period}</span>
                  </span>
                  <span className="block text-[15px]">{alarm.label}</span>
                </span>
                <IOSSwitch
                  checked={alarm.on}
                  label={`Alarm ${alarm.time} ${alarm.period}`}
                  onCheckedChange={(on) => setAlarms((list) => list.map((a) => (a.id === alarm.id ? { ...a, on } : a)))}
                />
              </div>
            ))}
          </IOSGroup>
        )}

        {tab === "stopwatch" && <Stopwatch />}
        {tab === "timer" && <Timer />}
      </IOSPage>

      <IOSTabBar items={tabs} value={tab} onValueChange={setTab} />

      <IOSSheet
        open={adding}
        onClose={() => setAdding(false)}
        title="Add Alarm"
        height="60%"
        left={<IOSNavButton onClick={() => setAdding(false)}>Cancel</IOSNavButton>}
        right={
          <IOSNavButton
            className="font-semibold"
            onClick={() => {
              setAlarms((list) => [...list, { id: list.length + 1, time: `${hour}:00`, period: "AM", label: "Alarm", on: true }])
              setAdding(false)
            }}
          >
            Save
          </IOSNavButton>
        }
      >
        <IOSGroup className="mt-4">
          <div className="flex items-center justify-between px-4 py-3">
            <label htmlFor="alarm-hour" className="text-[17px]">Hour</label>
            <select
              id="alarm-hour"
              value={hour}
              onChange={(event) => setHour(event.target.value)}
              className="rounded-lg bg-(color:--ios-fill) px-3 py-1.5 text-[17px] outline-none"
            >
              {[5, 6, 7, 8, 9, 10, 11].map((h) => <option key={h} value={h}>{h}:00 AM</option>)}
            </select>
          </div>
        </IOSGroup>
      </IOSSheet>
    </IOSDevice>
  )
}
```
