# iOS Camera

> Viewfinder, zoom, mode picker, shutter and recording timer.

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

## Install

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

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

import * as React from "react"
import { Circle, RefreshCw, Timer, Zap, ZapOff } from "lucide-react"

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

const modes = ["SLO-MO", "VIDEO", "PHOTO", "PORTRAIT", "PANO"]
const zooms = [0.5, 1, 2, 3]

export default function IOSCamera() {
  const [mode, setMode] = React.useState("PHOTO")
  const [zoom, setZoom] = React.useState(1)
  const [flash, setFlash] = React.useState(false)
  const [flashFrame, setFlashFrame] = React.useState(false)
  const [shots, setShots] = React.useState(0)
  const [recording, setRecording] = React.useState(false)
  const [seconds, setSeconds] = React.useState(0)
  const [front, setFront] = React.useState(false)
  const frameTimer = React.useRef<ReturnType<typeof setTimeout> | null>(null)

  const isVideo = mode === "VIDEO" || mode === "SLO-MO"

  React.useEffect(() => {
    if (!recording) return
    const id = setInterval(() => setSeconds((value) => value + 1), 1000)
    return () => clearInterval(id)
  }, [recording])

  React.useEffect(() => () => {
    if (frameTimer.current) clearTimeout(frameTimer.current)
  }, [])

  const shutter = () => {
    if (isVideo) {
      if (recording) setRecording(false)
      else {
        setSeconds(0)
        setRecording(true)
      }
      return
    }
    setShots((count) => count + 1)
    setFlashFrame(true)
    if (frameTimer.current) clearTimeout(frameTimer.current)
    frameTimer.current = setTimeout(() => setFlashFrame(false), 140)
  }

  const clock = `${String(Math.floor(seconds / 60)).padStart(2, "0")}:${String(seconds % 60).padStart(2, "0")}`

  return (
    <IOSDevice light className="bg-black text-white">
      <div className="relative mt-[54px] flex min-h-0 flex-1 flex-col">
        <div className="flex h-12 shrink-0 items-center justify-between px-5">
          <button
            type="button"
            aria-pressed={flash}
            aria-label="Flash"
            onClick={() => setFlash((value) => !value)}
            className={cn("flex size-9 items-center justify-center rounded-full active:scale-90", flash ? "bg-(color:--ios-yellow) text-black" : "bg-white/15")}
          >
            {flash ? <Zap className="size-[18px]" /> : <ZapOff className="size-[18px]" />}
          </button>
          {recording ? (
            <span className="flex items-center gap-1.5 rounded-md bg-(color:--ios-red) px-2 py-0.5 text-[15px] font-semibold tabular-nums">
              <span className="size-2 animate-pulse rounded-full bg-white" /> {clock}
            </span>
          ) : (
            <span className="text-[12px] font-medium text-white/50">{front ? "FRONT" : "BACK"} · {zoom}×</span>
          )}
          <button type="button" aria-label="Timer" className="flex size-9 items-center justify-center rounded-full bg-white/15 active:scale-90">
            <Timer className="size-[18px]" />
          </button>
        </div>

        <div className="relative mx-0 flex-1 overflow-hidden rounded-[6px] bg-black">
          <div
            aria-hidden
            className="absolute inset-0 transition-transform duration-500 ease-out"
            style={{ transform: `scale(${0.8 + zoom * 0.2})` }}
          >
            <div className={cn("absolute inset-0 bg-gradient-to-b", front ? "from-rose-300 via-orange-200 to-amber-100" : "from-sky-500 via-sky-300 to-amber-200")} />
            <div className="absolute inset-x-0 bottom-0 h-1/3 bg-gradient-to-t from-emerald-900 to-emerald-600/60" />
            <div className="absolute right-12 bottom-[28%] size-24 rounded-full bg-amber-200 blur-md" />
          </div>
          <div aria-hidden className="pointer-events-none absolute inset-0 grid grid-cols-3 grid-rows-3">
            {Array.from({ length: 9 }, (_, i) => <span key={i} className="border-[0.5px] border-white/25" />)}
          </div>
          <div aria-hidden className={cn("pointer-events-none absolute inset-0 bg-white transition-opacity", flashFrame ? "opacity-90 duration-75" : "opacity-0 duration-300")} />

          <div role="radiogroup" aria-label="Zoom" className="absolute inset-x-0 bottom-3 flex justify-center">
            <div className="flex gap-1 rounded-full bg-black/35 p-1 backdrop-blur-md">
              {zooms.map((z) => (
                <button
                  key={z}
                  type="button"
                  role="radio"
                  aria-checked={zoom === z}
                  onClick={() => setZoom(z)}
                  className={cn(
                    "flex size-9 items-center justify-center rounded-full text-[12px] font-semibold transition-all",
                    zoom === z ? "bg-black/60 text-(color:--ios-yellow)" : "text-white"
                  )}
                >
                  {z}
                  {zoom === z && "×"}
                </button>
              ))}
            </div>
          </div>
        </div>

        <div className="shrink-0 bg-black pb-8">
          <div role="tablist" aria-label="Camera mode" className="flex items-center justify-center gap-5 py-3">
            {modes.map((m) => (
              <button
                key={m}
                type="button"
                role="tab"
                aria-selected={mode === m}
                onClick={() => {
                  setMode(m)
                  setRecording(false)
                }}
                className={cn("shrink-0 text-[13px] font-semibold tracking-wide transition-colors", mode === m ? "text-(color:--ios-yellow)" : "text-white/80")}
              >
                {m}
              </button>
            ))}
          </div>

          <div className="flex items-center justify-between px-9">
            <span className="flex size-[50px] items-center justify-center overflow-hidden rounded-[10px] bg-gradient-to-br from-sky-400 to-emerald-500 text-[13px] font-semibold" role="img" aria-label={`${shots} photos taken`}>
              {shots > 0 ? shots : ""}
            </span>
            <button
              type="button"
              onClick={shutter}
              aria-label={isVideo ? (recording ? "Stop recording" : "Start recording") : "Take photo"}
              className="flex size-[74px] items-center justify-center rounded-full border-[4px] border-white active:scale-95"
            >
              {isVideo ? (
                <span className={cn("bg-(color:--ios-red) transition-all duration-200", recording ? "size-7 rounded-md" : "size-[58px] rounded-full")} />
              ) : (
                <Circle className="size-[60px] fill-white text-white" />
              )}
            </button>
            <button type="button" aria-label="Flip camera" onClick={() => setFront((value) => !value)} className="flex size-[50px] items-center justify-center rounded-full bg-white/15 active:rotate-180 active:scale-90">
              <RefreshCw className="size-6" />
            </button>
          </div>
        </div>
      </div>
    </IOSDevice>
  )
}
```
