# iOS Calculator

> A working dark calculator with orange operators.

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

## Install

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

Adds the page at `app/ios-calculator/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-calculator/page.tsx"
"use client"

import * as React from "react"

import { cn } from "@/lib/utils"
import { IOSDevice } from "@/components/ios-kit"

type Op = "+" | "−" | "×" | "÷"
type State = { display: string; acc: number | null; op: Op | null; fresh: boolean }

const initial: State = { display: "0", acc: null, op: null, fresh: true }

function compute(a: number, b: number, op: Op) {
  if (op === "+") return a + b
  if (op === "−") return a - b
  if (op === "×") return a * b
  return b === 0 ? NaN : a / b
}

function show(value: number) {
  if (!Number.isFinite(value)) return "Error"
  const text = String(Number(value.toPrecision(9)))
  return text
}

function group(display: string) {
  if (display === "Error" || display.includes("e")) return display
  const [int, frac] = display.split(".")
  const negative = int.startsWith("-")
  const body = (negative ? int.slice(1) : int).replace(/\B(?=(\d{3})+(?!\d))/g, ",")
  return `${negative ? "-" : ""}${body}${frac !== undefined ? `.${frac}` : ""}`
}

type Key = { label: string; kind: "digit" | "fn" | "op"; wide?: boolean }

const layout: Key[] = [
  { label: "AC", kind: "fn" }, { label: "±", kind: "fn" }, { label: "%", kind: "fn" }, { label: "÷", kind: "op" },
  { label: "7", kind: "digit" }, { label: "8", kind: "digit" }, { label: "9", kind: "digit" }, { label: "×", kind: "op" },
  { label: "4", kind: "digit" }, { label: "5", kind: "digit" }, { label: "6", kind: "digit" }, { label: "−", kind: "op" },
  { label: "1", kind: "digit" }, { label: "2", kind: "digit" }, { label: "3", kind: "digit" }, { label: "+", kind: "op" },
  { label: "0", kind: "digit", wide: true }, { label: ".", kind: "digit" }, { label: "=", kind: "op" },
]

export default function IOSCalculator() {
  const [state, setState] = React.useState<State>(initial)
  const { display, op, fresh, acc } = state
  const dirty = display !== "0" || acc !== null

  const press = (key: Key) => {
    setState((s) => {
      const { label } = key
      if (s.display === "Error" && label !== "AC" && label !== "C") return s
      if (key.kind === "digit") {
        if (label === ".") {
          if (s.fresh) return { ...s, display: "0.", fresh: false }
          return s.display.includes(".") ? s : { ...s, display: s.display + "." }
        }
        if (s.fresh) return { ...s, display: label, fresh: false }
        if (s.display.replace(/[-.]/g, "").length >= 9) return s
        return { ...s, display: s.display === "0" ? label : s.display + label }
      }
      if (label === "AC" || label === "C") return label === "C" ? { ...s, display: "0", fresh: true } : initial
      if (label === "±") return s.display === "0" ? s : { ...s, display: s.display.startsWith("-") ? s.display.slice(1) : "-" + s.display }
      if (label === "%") return { ...s, display: show(Number(s.display) / 100), fresh: true }

      const current = Number(s.display)
      const result = s.op && s.acc !== null && !s.fresh ? compute(s.acc, current, s.op) : current
      const text = show(result)
      if (label === "=") return { display: text, acc: null, op: null, fresh: true }
      return { display: text, acc: Number.isFinite(result) ? result : null, op: label as Op, fresh: true }
    })
  }

  return (
    <IOSDevice light className="bg-black text-white">
      <div className="flex min-h-0 flex-1 flex-col justify-end px-4 pb-[46px]">
        <p
          aria-live="polite"
          aria-label="Result"
          className={cn(
            "mb-3 truncate px-2 text-right font-light tracking-tight tabular-nums",
            display.length > 9 ? "text-[56px]" : display.length > 6 ? "text-[72px]" : "text-[92px]"
          )}
          style={{ lineHeight: 1.1 }}
        >
          {group(display)}
        </p>
        <div className="grid grid-cols-4 gap-3">
          {layout.map((key) => {
            const active = key.kind === "op" && key.label === op && fresh
            const label = key.label === "AC" && dirty && !fresh ? "C" : key.label
            return (
              <button
                key={key.label}
                type="button"
                onClick={() => press({ ...key, label })}
                className={cn(
                  "flex h-[80px] items-center rounded-full text-[34px] font-normal transition-[background-color,color,filter] duration-300 active:brightness-125 active:duration-0 max-sm:h-[18vw]",
                  key.wide ? "col-span-2 justify-start pl-[30px]" : "justify-center",
                  key.kind === "digit" && "bg-[#333333]",
                  key.kind === "fn" && "bg-[#a5a5a5] text-black",
                  key.kind === "op" && (active ? "bg-white text-[#ff9f0a]" : "bg-[#ff9f0a]")
                )}
                aria-label={key.label === "−" ? "Subtract" : key.label === "÷" ? "Divide" : key.label === "×" ? "Multiply" : undefined}
              >
                <span className={key.kind === "op" ? "text-[42px]" : undefined}>{label}</span>
              </button>
            )
          })}
        </div>
      </div>
    </IOSDevice>
  )
}
```
