Build a Timer with Claude Code

Build and test a browser timer while supervising Claude Code.

Introduction

30 Second Summary

A focus session can fall apart when interruptions leave you with no reliable way to pause or resume. Coding with an AI assistant creates similar friction when changes arrive faster than you can review them.

In this project, you will build a Focus Sprint Timer with Claude Code. You will supervise each change from planning through verification.

What You'll Build

Your finished timer opens in Safari as a live countdown with controls that stay dependable throughout every focus session.

By the end of this project, you'll have:

  • Flexible focus sessions you can set from 1 to 60 minutes. You can start, pause, resume, or reset each session while the clock and progress bar stay synchronized.
  • A browser test harness you can open to see seven deterministic checks report 7 passed, 0 failed.
  • A reusable workflow built around Plan mode, diff review, browser verification, and project memory in CLAUDE.md.
  • Secret Mission: Add one-click 5, 15, and 25 minute presets without disturbing an active session.

Are there any prerequisites?

You need a Mac with an active Claude Pro or Max subscription. The guide handles Claude Code setup using Terminal and Safari.

Before We Start

Before the hands-on work begins, lock in the app you want to build and the development habit you want to practice. This gives every planning, implementation, diff review, and browser verification decision a clear purpose.

Set Up Claude Code and the Project

You have defined the Focus Sprint Timer as a way to protect a focused block of work. Now your coding agent needs a safe local boundary before it can help build that app.

Claude Code works directly with local files inside the directory where you start it. Terminal lets you verify the installation before the agent touches the project folder.

In this step, get ready to:
  • Confirm that Claude Code is version 2.1.293 or later.
  • Create an empty project folder at ~/focus-sprint-timer.
  • Authenticate a Claude Code session from the project folder.
Verify your Claude Code installation

The version check shows whether your Mac already has the required Claude Code build. Your result determines whether you can continue or need the native installer.

  • Open macOS application search.
  • Type Terminal in the search field.
  • Select Terminal from the results.
  • Check your current Claude Code installation by running this command:
claude --version

What does this check prove?

The claude --version command identifies the installed Claude Code build. Its result tells you whether the native installer needs to run.

✔️ I see version 2.1.293 or higher

Your existing installation meets the project requirement. Claude Code is ready for the local project folder.

ⓧ I see an older version

Your installed build predates version 2.1.293. The native installer updates it to the current build.

  • Update Claude Code by running this command:
curl -fsSL https://claude.ai/install.sh | bash

What does this installer do?

This is Anthropic's native macOS installer for Claude Code. It updates the command-line application without adding dependencies to your timer project.

  • Close the current Terminal window after the installer finishes.
  • Open macOS application search.
  • Type Terminal in the search field.
  • Select Terminal from the results.

ⓧ Command not found

Claude Code is not available in your current Terminal environment. The native installer adds it to your Mac.

  • Install Claude Code by running this command:
curl -fsSL https://claude.ai/install.sh | bash

What does this installer do?

This is Anthropic's native macOS installer for Claude Code. It installs the command-line application without adding dependencies to your timer project.

  • Close the current Terminal window after the installer finishes.
  • Open macOS application search.
  • Type Terminal in the search field.
  • Select Terminal from the results.

Before you run the check again, which version result do you expect after following your tab?

  • Confirm the installed version by running this command:
claude --version

What does this final check prove?

This second check verifies that the required Claude Code build is available in the fresh Terminal environment. It gives you evidence before you create the project workspace.

You should see a version number followed by (Claude Code). The version number should be 2.1.293 or higher.

That is the first setup hurdle cleared. Your coding agent is installed and responding in Terminal.

Version check still not ready?

  • Confirm that your Mac has an internet connection before repeating the installer.
  • Close Terminal after the installation finishes.
  • Open a fresh Terminal window before checking the version again.

Need a hand? Help me diagnose why Claude Code is unavailable after using the native macOS installer. You can also share the version output in the NextWork community.

Create the project folder

A dedicated folder limits the files available in the project workspace. The empty ~/focus-sprint-timer folder gives Claude Code a clear starting boundary.

  • Create the project folder and move into it by running these commands:
mkdir -p ~/focus-sprint-timer
cd ~/focus-sprint-timer

What do these commands do?

  • The mkdir -p command creates ~/focus-sprint-timer if the folder does not already exist.
  • The cd command moves the current Terminal shell into that folder.

Your Terminal shell is now positioned inside the empty ~/focus-sprint-timer folder.

Folder command did not work?

  • Check that the folder path is spelled ~/focus-sprint-timer.
  • Confirm that Terminal is running under your own macOS account.

Still stuck? Help me create and enter the focus-sprint-timer folder in my macOS home directory.

Authenticate inside the project folder

Claude Code uses the directory where you start its session as the initial project scope. Starting from the current shell keeps the agent focused on the timer folder.

The first session opens browser authentication when needed. Your existing Claude Pro or Max plan covers this project, so you do not need a separate API purchase.

Before you start the session, which working directory should Claude report from the shell location you just set?

  • Start Claude Code inside the project folder by running this command:
claude

What does this command do?

The claude command starts an interactive Claude Code session from your current directory. On first use, the session opens browser authentication for your account.

  • Complete browser sign-in with the same Claude Pro or Max account you already use.
  • Return to the Claude Code session in Terminal.
  • Leave every file change unapproved during this directory check.
  • Ask Claude which working directory it can see.

Claude should report ~/focus-sprint-timer as its working directory. That response proves the session is scoped to the empty project folder.

You now have an authenticated coding agent with a clearly confirmed boundary. Claude Code can help without reaching into an unrelated folder.

Session opened in the wrong folder?

  • Exit the Claude Code session.
  • Run the folder commands from the previous substep again.
  • Start Claude Code again from that Terminal shell.
  • Enter /login inside the session if you need to authenticate with a different account.

Need help? Help me confirm why my Claude Code session reports the wrong working directory.

Your Claude Code session is authenticated inside an empty project folder. Next, you will use Plan mode to build the timer's first visible slice.

Build the Start-Only Timer

Your Claude Code session is authenticated inside ~/focus-sprint-timer. You now have a safe boundary for its first coding task.

An empty folder gives you nothing to inspect or verify. This first slice creates a visible timer in Safari while keeping every decision small enough to review.

In this step, get ready to:
  • Use Plan mode to define a dependency-free first slice.
  • Review the proposed file responsibilities and implementation diffs.
  • Run the timer in Safari to verify the countdown and expose its start-only limitation.
Plan the first working slice

Plan mode lets Claude Code inspect the project and reason about an approach without changing files. This separates your design decision from the implementation.

  • Switch back to the Claude Code session from earlier.
  • Press Shift+Tab until the status bar shows that Plan mode is active.
  • Ask Claude to plan a dependency-free timer using index.html, style.css, timer.js, and app.js.
  • Require a duration input that accepts 1 through 60 minutes.
  • Require a visible countdown formatted as a clock.
  • Require a progress bar that tracks elapsed time.
  • Limit the controls to one Start button.
  • Require the app to work by opening index.html directly in Safari.

Why plan before editing?

A plan gives you a checkpoint before code reaches the project. You can remove extra technology while changes are still cheap.

The small scope also makes every later diff explainable. You stay in charge of what enters the codebase.

  • Ask Claude to summarize the responsibility of each proposed file.
  • Review the proposed file responsibilities against the timer requirements.
  • Reject any framework, package, server, or extra feature.
  • Approve the plan after it matches the requested scope.
  • Leave Plan mode by pressing Shift+Tab.

Claude now has an approved boundary for the first slice. The plan covers one visible behavior without expanding the project.

Implement and inspect the timer

Implementation turns the approved plan into local files. Diff review is your chance to catch scope drift before accepting generated work.

  • Ask Claude to implement exactly the approved scope.
  • Require Claude to present each proposed diff for review.
  • Inspect index.html for the timer structure and matching script references.
  • Inspect style.css for the centered dark timer interface.
  • Inspect timer.js for duration clamping and time formatting inside window.TimerUtils.
  • Inspect app.js for the countdown state and Start behavior.
  • Reject any diff that introduces an unapproved dependency or control.
  • Accept each file after its diff matches the approved plan.

✔️ Awesome, I've got everything!

Great. Your four approved files now form one start-only timer that runs directly in the browser.

ⓧ I'd like to double check the full code

The complete files should read as follows. Each identifier and value forms the checkpoint for this first slice.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Focus Sprint Timer</title>
    <link rel="stylesheet" href="style.css" />
    <script defer src="timer.js"></script>
    <script defer src="app.js"></script>
  </head>
  <body>
    <main class="timer-card">
      <p class="eyebrow">Focus Sprint</p>
      <h1>Make the next minutes count.</h1>
      <p class="intro">Choose a duration, start the clock, and protect one small block of focused work.</p>

      <section class="timer-panel" aria-label="Focus timer">
        <div id="timer-display" class="timer-display" aria-live="polite">25:00</div>

        <label for="duration-input">Minutes</label>
        <input id="duration-input" type="number" min="1" max="60" value="25" />

        <progress id="progress" max="1500" value="0" aria-label="Sprint progress"></progress>

        <div class="controls">
          <button id="start-button" class="primary" type="button">Start</button>
        </div>

        <p id="status" class="status" aria-live="polite">Ready</p>
      </section>
    </main>
  </body>
</html>

What does the page define?

  • The page loads style.css before the browser displays the interface.
  • The deferred scripts load timer.js before app.js.
  • The element identifiers give app.js stable targets for the display, input, progress bar, button, and status.
:root {
  color-scheme: dark;
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  background: #0b1020;
  color: #f7f8ff;
}

* {
  box-sizing: border-box;
}

body {
  min-height: 100vh;
  margin: 0;
  display: grid;
  place-items: center;
  padding: 24px;
  background:
    radial-gradient(circle at top, rgba(124, 92, 255, 0.35), transparent 38%),
    #0b1020;
}

.timer-card {
  width: min(100%, 560px);
  padding: 40px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 28px;
  background: rgba(17, 24, 48, 0.92);
  box-shadow: 0 28px 80px rgba(0, 0, 0, 0.35);
}

.eyebrow {
  margin: 0 0 8px;
  color: #a99cff;
  font-size: 0.78rem;
  font-weight: 800;
  letter-spacing: 0.16em;
  text-transform: uppercase;
}

h1 {
  margin: 0;
  font-size: clamp(2rem, 7vw, 3.5rem);
  line-height: 1;
}

.intro {
  margin: 16px 0 32px;
  color: #b8bfd8;
  line-height: 1.6;
}

.timer-panel {
  display: grid;
  gap: 16px;
}

.timer-display {
  font-variant-numeric: tabular-nums;
  font-size: clamp(4rem, 18vw, 7rem);
  font-weight: 800;
  letter-spacing: -0.06em;
  text-align: center;
}

label {
  color: #cbd1e7;
  font-size: 0.9rem;
  font-weight: 700;
}

input {
  width: 100%;
  padding: 12px 14px;
  border: 1px solid #394265;
  border-radius: 12px;
  background: #10172d;
  color: #ffffff;
  font: inherit;
}

progress {
  width: 100%;
  height: 12px;
  accent-color: #8b75ff;
}

.controls {
  display: grid;
  gap: 10px;
}

button {
  padding: 12px 14px;
  border: 1px solid #465074;
  border-radius: 12px;
  background: #1a2340;
  color: #ffffff;
  font: inherit;
  font-weight: 800;
  cursor: pointer;
}

button.primary {
  border-color: #8b75ff;
  background: #7c5cff;
}

.status {
  min-height: 24px;
  margin: 0;
  color: #aeb7d3;
  text-align: center;
}

What does the styling provide?

  • The page centers a single timer card against a dark background.
  • The large numeric display keeps the remaining time visible at a glance.
  • The input, progress bar, button, and status share consistent spacing inside the timer panel.
(function () {
  const DEFAULT_MINUTES = 25;
  const MIN_MINUTES = 1;
  const MAX_MINUTES = 60;

  function clampMinutes(value) {
    const numericValue = Number(value);

    if (!Number.isFinite(numericValue)) {
      return DEFAULT_MINUTES;
    }

    return Math.min(MAX_MINUTES, Math.max(MIN_MINUTES, Math.round(numericValue)));
  }

  function formatTime(totalSeconds) {
    const safeSeconds = Math.max(0, Math.floor(Number(totalSeconds) || 0));
    const minutes = Math.floor(safeSeconds / 60);
    const seconds = safeSeconds % 60;

    return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
  }

  window.TimerUtils = {
    clampMinutes,
    formatTime,
  };
})();

What do the timer utilities handle?

  • The minute constants define the default duration and the accepted range.
  • The clampMinutes() function converts input into a whole minute value between 1 and 60.
  • The formatTime() function converts seconds into a two-part clock display.
  • The window.TimerUtils object makes both helpers available to app.js.
const timerDisplay = document.getElementById("timer-display");
const durationInput = document.getElementById("duration-input");
const progress = document.getElementById("progress");
const startButton = document.getElementById("start-button");
const status = document.getElementById("status");

let durationSeconds = 25 * 60;
let remainingSeconds = durationSeconds;
let timerId = null;
let statusMessage = "Ready";

function render() {
  timerDisplay.textContent = window.TimerUtils.formatTime(remainingSeconds);
  progress.max = durationSeconds;
  progress.value = durationSeconds - remainingSeconds;
  status.textContent = statusMessage;
  startButton.disabled = timerId !== null || remainingSeconds === 0;
}

function syncDurationFromInput() {
  const minutes = window.TimerUtils.clampMinutes(durationInput.value);
  durationInput.value = String(minutes);
  durationSeconds = minutes * 60;
  remainingSeconds = durationSeconds;
  render();
}

function tick() {
  remainingSeconds = Math.max(0, remainingSeconds - 1);

  if (remainingSeconds === 0) {
    window.clearInterval(timerId);
    timerId = null;
    statusMessage = "Sprint complete";
  }

  render();
}

function startTimer() {
  if (timerId !== null || remainingSeconds === 0) {
    return;
  }

  syncDurationFromInput();
  statusMessage = "Focusing";
  timerId = window.setInterval(tick, 1000);
  render();
}

durationInput.addEventListener("change", syncDurationFromInput);
startButton.addEventListener("click", startTimer);

render();

How does the first slice run?

  • The DOM references connect the JavaScript state to the visible timer elements.
  • The render() function keeps the clock, progress bar, status, and Start button synchronized.
  • The tick() function removes one second during each interval.
  • The startTimer() function reads the duration before starting the repeated countdown.

Do the diffs exceed the plan?

  • Tell Claude to remove any framework, package, or server from the proposal.
  • Check that the only control in index.html is the Start button.
  • Ask Claude to revise mismatched identifiers before accepting the affected file.

Still seeing unexpected changes? Help me compare Claude Code's proposed timer diffs with an approved four-file start-only plan.

Run the timer and expose the gap

Browser verification turns reviewed code into observable evidence. You will test the timer through the same interface a user sees.

  • Click Finder in the Dock to open a window.
  • Press Shift-Command-G to open the Go to Folder window.
  • Enter ~/focus-sprint-timer in the pathname field.
  • Press Return.
  • Select index.html.
  • Choose File from the Finder menu bar.
  • Choose Open With.
  • Choose Safari.

Your first slice is live. You should see a dark Focus Sprint card with 25:00, a Minutes input, a progress bar, a Start button, and the Ready status.

Does the timer look unstyled or incomplete?

  • Confirm that all four files appear together inside ~/focus-sprint-timer.
  • Check that index.html references style.css, timer.js, and app.js with matching names.
  • Ask Claude to compare the accepted files with the full-code checkpoint above.

Need help finding the mismatch? Help me diagnose why my local Focus Sprint Timer looks unstyled or incomplete in Safari.

  • Enter 1 in the Minutes input.

Before you select Start, how do you expect the clock and status to change?

  • Select Start.

After one second, you should see the display change from 01:00 to 00:59. The status reads Focusing while the progress bar begins to move.

That countdown is your first visible win. The approved plan now runs as a real browser app.

Before you try to interrupt the sprint, what control do you expect to reach for?

  • Try to pause the running timer from the page.

You will find only the disabled Start button. There is no pause or reset control, so the running session cannot be interrupted from the page.

This trapped-session shortfall is intentional. You now have an observed problem that can become precise acceptance criteria.

Your first timer slice works and its limitation is visible. Next, you will turn that evidence into a controlled change.

Fix the Missing Control Flow

Your start-only timer proved that Claude Code can turn an approved plan into a working browser app. It also exposed the intended flaw: once a sprint starts, the user is trapped until the countdown ends.

Now you'll turn that shortfall into precise acceptance criteria. You'll supervise a controlled change through implementation, diff review, and browser verification.

In this step, get ready to:
  • Give Claude the observed timer problem and clear acceptance criteria.
  • Review each proposed file change before accepting it.
  • Verify pause, resume, reset, and duration limits in the browser.
Turn the shortfall into acceptance criteria

Acceptance criteria describe the behavior that must be true when a change is complete. They give Claude a boundary for the implementation and give you a checklist for reviewing the result.

  • Return to the Claude Code session from the previous step.
  • Describe the observed symptom: a running sprint cannot be paused or reset.
  • Require a Pause control that stops the countdown without changing the remaining time.
  • Require a Reset control that restores the selected duration with a Ready status.
  • Require the Start control to become Resume after pausing.
  • Require the Minutes field to become disabled after a sprint starts.
  • Require entered durations to stay between 1 and 60 minutes.
  • Require the current visual style to stay intact.

Why use acceptance criteria?

The observed symptom tells Claude what is wrong. The criteria define exactly what a successful repair must do.

This keeps the request scoped to the missing control flow. It also gives you concrete evidence to look for in the diff.

  • Require the timer to report Focusing, Paused, Sprint complete, or Ready as its state changes.
  • Ask Claude to implement only these criteria in the existing project files.
  • Reject any new framework or package from the proposal.
  • Review Claude's proposed file list before approving any write.

Claude's proposal should stay within the four existing files. The proposed changes should extend the current timer instead of replacing its dependency-free structure.

Review and accept the scoped diff

A diff shows the exact lines Claude wants to add, remove, or replace. This review can feel fiddly because a small state change can affect several files, so inspect one file at a time.

  • Inspect the proposed index.html diff in Claude Code.
  • Confirm it adds pause-button and reset-button inside .controls.
  • Inspect the proposed style.css diff.
  • Confirm the controls use three equal columns on wider screens.
  • Confirm disabled controls use the shared dimmed style.
  • Confirm the mobile rule stacks the controls into one column.
  • Inspect the proposed timer.js diff.
  • Reject any change to timer.js because its existing helpers already format time and clamp durations.

Why does timer.js stay unchanged?

The existing clampMinutes() helper already normalizes durations to the required range. The existing formatTime() helper already produces the clock display.

Reusing these helpers keeps the new behavior inside the interface control flow. That is a smaller change with fewer regression risks.

  • Inspect the proposed app.js diff.
  • Ask Claude to explain the purpose of every state variable in app.js before approval.
  • Confirm the diff introduces hasStarted, stopInterval(), pauseTimer(), and resetTimer().
  • Confirm the diff registers click listeners for pauseButton and resetButton.
  • Accept the proposed changes after every criterion is represented.
  • Return to Safari from the previous step.
  • Reload index.html.

You should see Start, Pause, and Reset controls. The Pause control starts disabled while the status reads Ready.

Good progress. Your timer now exposes every control needed to manage a sprint, while the behavior still needs a full manual check.

  • Choose the first tab if your accepted diff matches the criteria.

✔️ Awesome, I've got everything!

Your accepted changes match the scoped control-flow update. Continue to the manual sequence to prove every state transition works.

ⓧ I'd like to double check the full code

  • Compare each accepted file with the complete references below.
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Focus Sprint Timer</title>
    <link rel="stylesheet" href="style.css" />
    <script defer src="timer.js"></script>
    <script defer src="app.js"></script>
  </head>
  <body>
    <main class="timer-card">
      <p class="eyebrow">Focus Sprint</p>
      <h1>Make the next minutes count.</h1>
      <p class="intro">Choose a duration, start the clock, and protect one small block of focused work.</p>

      <section class="timer-panel" aria-label="Focus timer">
        <div id="timer-display" class="timer-display" aria-live="polite">25:00</div>

        <label for="duration-input">Minutes</label>
        <input id="duration-input" type="number" min="1" max="60" value="25" />

        <progress id="progress" max="1500" value="0" aria-label="Sprint progress"></progress>

        <div class="controls">
          <button id="start-button" class="primary" type="button">Start</button>
          <button id="pause-button" type="button" disabled>Pause</button>
          <button id="reset-button" type="button">Reset</button>
        </div>

        <p id="status" class="status" aria-live="polite">Ready</p>
      </section>
    </main>
  </body>
</html>

What does this structure add?

The control group now contains the new Pause and Reset buttons. Their identifiers give app.js stable elements to control.

:root {
  color-scheme: dark;
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  background: #0b1020;
  color: #f7f8ff;
}

* {
  box-sizing: border-box;
}

body {
  min-height: 100vh;
  margin: 0;
  display: grid;
  place-items: center;
  padding: 24px;
  background:
    radial-gradient(circle at top, rgba(124, 92, 255, 0.35), transparent 38%),
    #0b1020;
}

.timer-card {
  width: min(100%, 560px);
  padding: 40px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 28px;
  background: rgba(17, 24, 48, 0.92);
  box-shadow: 0 28px 80px rgba(0, 0, 0, 0.35);
}

.eyebrow {
  margin: 0 0 8px;
  color: #a99cff;
  font-size: 0.78rem;
  font-weight: 800;
  letter-spacing: 0.16em;
  text-transform: uppercase;
}

h1 {
  margin: 0;
  font-size: clamp(2rem, 7vw, 3.5rem);
  line-height: 1;
}

.intro {
  margin: 16px 0 32px;
  color: #b8bfd8;
  line-height: 1.6;
}

.timer-panel {
  display: grid;
  gap: 16px;
}

.timer-display {
  font-variant-numeric: tabular-nums;
  font-size: clamp(4rem, 18vw, 7rem);
  font-weight: 800;
  letter-spacing: -0.06em;
  text-align: center;
}

label {
  color: #cbd1e7;
  font-size: 0.9rem;
  font-weight: 700;
}

input {
  width: 100%;
  padding: 12px 14px;
  border: 1px solid #394265;
  border-radius: 12px;
  background: #10172d;
  color: #ffffff;
  font: inherit;
}

progress {
  width: 100%;
  height: 12px;
  accent-color: #8b75ff;
}

.controls {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
}

button {
  padding: 12px 14px;
  border: 1px solid #465074;
  border-radius: 12px;
  background: #1a2340;
  color: #ffffff;
  font: inherit;
  font-weight: 800;
  cursor: pointer;
}

button.primary {
  border-color: #8b75ff;
  background: #7c5cff;
}

button:disabled,
input:disabled {
  cursor: not-allowed;
  opacity: 0.45;
}

.status {
  min-height: 24px;
  margin: 0;
  color: #aeb7d3;
  text-align: center;
}

@media (max-width: 520px) {
  .timer-card {
    padding: 28px 20px;
  }

  .controls {
    grid-template-columns: 1fr;
  }
}

What does this styling change?

The controls form three equal columns on wider screens. The mobile rule stacks them for a narrower display.

Disabled buttons and inputs share the same dimmed appearance. That makes unavailable actions visible at a glance.

(function () {
  const DEFAULT_MINUTES = 25;
  const MIN_MINUTES = 1;
  const MAX_MINUTES = 60;

  function clampMinutes(value) {
    const numericValue = Number(value);

    if (!Number.isFinite(numericValue)) {
      return DEFAULT_MINUTES;
    }

    return Math.min(MAX_MINUTES, Math.max(MIN_MINUTES, Math.round(numericValue)));
  }

  function formatTime(totalSeconds) {
    const safeSeconds = Math.max(0, Math.floor(Number(totalSeconds) || 0));
    const minutes = Math.floor(safeSeconds / 60);
    const seconds = safeSeconds % 60;

    return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
  }

  window.TimerUtils = {
    clampMinutes,
    formatTime,
  };
})();

Why keep this file unchanged?

This file already provides the duration boundary logic and clock formatting. The control update reuses those tested responsibilities without duplicating them.

const timerDisplay = document.getElementById("timer-display");
const durationInput = document.getElementById("duration-input");
const progress = document.getElementById("progress");
const startButton = document.getElementById("start-button");
const pauseButton = document.getElementById("pause-button");
const resetButton = document.getElementById("reset-button");
const status = document.getElementById("status");

let durationSeconds = 25 * 60;
let remainingSeconds = durationSeconds;
let timerId = null;
let hasStarted = false;
let statusMessage = "Ready";

function render() {
  timerDisplay.textContent = window.TimerUtils.formatTime(remainingSeconds);
  progress.max = durationSeconds;
  progress.value = durationSeconds - remainingSeconds;
  status.textContent = statusMessage;

  durationInput.disabled = hasStarted;
  startButton.disabled = timerId !== null || remainingSeconds === 0;
  startButton.textContent = hasStarted ? "Resume" : "Start";
  pauseButton.disabled = timerId === null;
}

function syncDurationFromInput() {
  const minutes = window.TimerUtils.clampMinutes(durationInput.value);
  durationInput.value = String(minutes);
  durationSeconds = minutes * 60;
  remainingSeconds = durationSeconds;
  render();
}

function stopInterval() {
  if (timerId !== null) {
    window.clearInterval(timerId);
    timerId = null;
  }
}

function tick() {
  remainingSeconds = Math.max(0, remainingSeconds - 1);

  if (remainingSeconds === 0) {
    stopInterval();
    statusMessage = "Sprint complete";
  }

  render();
}

function startTimer() {
  if (timerId !== null || remainingSeconds === 0) {
    return;
  }

  if (!hasStarted) {
    syncDurationFromInput();
    hasStarted = true;
  }

  statusMessage = "Focusing";
  timerId = window.setInterval(tick, 1000);
  render();
}

function pauseTimer() {
  if (timerId === null) {
    return;
  }

  stopInterval();
  statusMessage = "Paused";
  render();
}

function resetTimer() {
  stopInterval();
  hasStarted = false;
  statusMessage = "Ready";
  syncDurationFromInput();
}

durationInput.addEventListener("change", () => {
  if (!hasStarted) {
    syncDurationFromInput();
  }
});

startButton.addEventListener("click", startTimer);
pauseButton.addEventListener("click", pauseTimer);
resetButton.addEventListener("click", resetTimer);

render();

How does the control flow work?

The hasStarted flag records whether the current session has begun. The timerId value records whether the countdown is actively running.

Pause clears the active interval while preserving remainingSeconds. Reset clears the session state before synchronizing the selected duration.

Seeing an unexpected diff?

  • Check that Claude edited the existing files instead of creating replacements with different names.
  • Reject any change that adds a package or framework to this dependency-free project.
  • Ask Claude to revise the diff if an identifier differs from the matching element in index.html.

Still stuck? Help me compare my Focus Sprint Timer diff with the required pause, resume, and reset behavior.

Run the complete control sequence

The manual sequence checks transitions between running, paused, resumed, and ready states. It also proves that pausing preserves the exact remaining time.

Before you test the controls, which values do you expect to freeze when you pause the timer?

  • Reload index.html in Safari.
  • Enter 1 in the Minutes field.
  • Click Start.
  • Wait until the clock reads 00:59.
  • Click Pause.
  • Wait for two seconds.
  • Check the clock value after the wait.

You should still see 00:59 after two seconds. The status reads Paused, which proves the interval stopped without changing the remaining time.

Before you continue, what should happen to the frozen value after Resume and Reset?

  • Click Resume.
  • Wait until the clock changes from 00:59.
  • Click Reset.

You should see 01:00 with a Ready status. The Minutes field is enabled again while the primary control reads Start.

Before you check the input limits, what should the timer do with values outside the allowed range?

  • Enter 0 in the Minutes field.
  • Click the large timer display to leave the field.

You should see the field normalize to 1 while the clock shows 01:00.

  • Enter 61 in the Minutes field.
  • Click the large timer display to leave the field.

You should see the field normalize to 60 while the clock shows 60:00. The invalid-value fallback receives direct automated coverage in the next step.

That is the control-flow gap closed: your timer now pauses without losing time, resumes from the frozen value, and resets cleanly. Next up, you'll replace manual confidence with repeatable browser tests and persistent project instructions.

Add Verification and Project Memory

Your timer now handles a full session in Safari. The manual sequence proved that Start, Pause, Resume, and Reset work together.

A manual happy-path check can miss regressions. A browser test harness gives every timer rule repeatable pass or fail evidence.

Concise project memory gives future sessions the same engineering rules. Your Claude Code workflow becomes easier to verify after this step.

In this step, get ready to:
  • Create a dependency-free browser test harness for the timer utilities.
  • Review seven deterministic checks before accepting the new files.
  • Make the project conventions persist through CLAUDE.md.
Create the browser test harness

The timer logic already lives in timer.js. A separate test page can call those utilities with fixed inputs without waiting through a real timer session.

  • In the Claude Code session from earlier, ask Claude to create tests.html and tests.js without external libraries.
  • Require tests.html to load style.css, timer.js, and tests.js in that order.
  • Require seven checks for zero formatting, minute and second formatting, negative input, both duration bounds, rounding, and invalid input fallback.
  • Review every proposed test case before accepting it.
  • Confirm that each assertion compares a concrete actual value with a concrete expected value.

Why Use Browser Tests?

The browser harness runs the same TimerUtils functions that power the app. This keeps the verification close to real browser behavior.

The harness opens directly in Safari. The project stays dependency-free.

✔️ Awesome, I've got everything!

Your proposed diff contains a test page plus seven deterministic JavaScript checks.

ⓧ I'd like to double check the full code

  • Compare the proposed tests.html file with this reference:
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Focus Sprint Timer Tests</title>
    <link rel="stylesheet" href="style.css" />
    <script defer src="timer.js"></script>
    <script defer src="tests.js"></script>
  </head>
  <body>
    <main class="timer-card">
      <p class="eyebrow">Verification</p>
      <h1>Timer tests</h1>
      <p id="test-summary" class="intro">Running tests...</p>
      <ul id="test-results"></ul>
    </main>
  </body>
</html>

How Does the Test Page Work?

  • The page reuses style.css so the results match the timer's visual design.
  • The page loads timer.js before tests.js so the test code can access window.TimerUtils.
  • The test-summary and test-results elements make the outcome visible in Safari.
  • Compare the proposed tests.js file with this reference:
const tests = [
  ["formats zero", () => assertEqual(window.TimerUtils.formatTime(0), "00:00")],
  ["formats minutes and seconds", () => assertEqual(window.TimerUtils.formatTime(65), "01:05")],
  ["prevents negative display", () => assertEqual(window.TimerUtils.formatTime(-5), "00:00")],
  ["clamps below minimum", () => assertEqual(window.TimerUtils.clampMinutes(0), 1)],
  ["clamps above maximum", () => assertEqual(window.TimerUtils.clampMinutes(61), 60)],
  ["rounds fractional minutes", () => assertEqual(window.TimerUtils.clampMinutes(24.6), 25)],
  ["uses fallback for invalid input", () => assertEqual(window.TimerUtils.clampMinutes("not-a-number"), 25)],
];

function assertEqual(actual, expected) {
  if (actual !== expected) {
    throw new Error(`Expected ${expected}, received ${actual}`);
  }
}

const results = document.getElementById("test-results");
const summary = document.getElementById("test-summary");
let passed = 0;

for (const [name, test] of tests) {
  const item = document.createElement("li");

  try {
    test();
    passed += 1;
    item.textContent = `PASS: ${name}`;
  } catch (error) {
    item.textContent = `FAIL: ${name}. ${error.message}`;
  }

  results.appendChild(item);
}

summary.textContent = `${passed} passed, ${tests.length - passed} failed`;

What Does the Test Harness Cover?

  • The tests array holds the seven named checks.
  • The assertEqual() function throws when an actual result differs from the expected result.
  • The loop records a visible pass or failure for every check.
  • The final summary reports the total numbers through passed and tests.length.
  • Ask Claude to correct any mismatch before you continue.
  • Accept the proposed tests.html diff.
  • Accept the proposed tests.js diff.
Run and repair the browser checks

Deterministic checks always use the same inputs. A failure therefore points to a specific behavior change instead of timer timing.

Before you open the test page, make a mental prediction about whether all seven checks will pass on the first run.

  • Return to the Finder window for ~/focus-sprint-timer from the previous step.
  • Drag tests.html into the Safari window from earlier.

You should see 7 passed, 0 failed above seven passing checks. That result gives your timer a repeatable regression signal.

Seeing a Failed Check?

A failed check identifies a mismatch between the expected timer rule and the current implementation. Keep the assertion intact while you investigate the cause.

  • Copy the exact failed line from tests.html.
  • Return to the Claude Code session from earlier.
  • Paste the failure into Claude Code.
  • Require a root-cause fix without weakening the test.
  • Review the proposed diff before accepting it.
  • Refresh tests.html in Safari.

Still stuck? Help me fix a failing browser test without weakening the assertion.

Add persistent project instructions

A new Claude Code conversation needs the same boundaries you used in this one. The CLAUDE.md file stores concise rules that Claude Code loads as project context.

  • Return to the authenticated Claude Code session from earlier.
  • Enter /init to generate a starter CLAUDE.md inside ~/focus-sprint-timer.

Claude Code creates a starter project instruction file. You now have a dedicated place for rules that apply across conversations.

  • Ask Claude to replace the generated boilerplate with three concise project rules.
  • Require the app to stay dependency-free with direct Safari use.
  • Require browser-test verification after timer behavior changes.
  • Require every DOM identifier to stay synchronized across HTML and JavaScript.
  • Review the proposed CLAUDE.md diff.
  • Accept the diff after all generated boilerplate has been removed.

✔️ Awesome, I've got everything!

Your project memory contains three short rules for architecture, testing, and identifier consistency.

ⓧ I'd like to double check the full code

  • Compare CLAUDE.md with this complete reference:
# Project instructions

- Keep the app dependency-free and runnable by opening `index.html` directly in Safari.
- After changing timer behavior, preserve or update `tests.js` and verify `tests.html` reports zero failures.
- Keep every DOM identifier synchronized across HTML and JavaScript.

What Do These Instructions Protect?

  • The first rule preserves the direct Safari workflow.
  • The second rule makes browser tests part of every timer behavior change.
  • The third rule prevents mismatches between HTML identifiers and JavaScript lookups.

Before entering /context, make a mental prediction about which project instruction file should appear in the loaded context.

  • Enter /context in the current Claude Code session.

You should see CLAUDE.md in the loaded project context. Your tests and project rules now give future changes a clear verification path.

Do Not See CLAUDE.md in Context?

  • Confirm that Claude Code is still working inside ~/focus-sprint-timer.
  • Confirm that the filename is exactly CLAUDE.md.
  • Enter /context again after saving the file.

Need another set of eyes? Help me find why CLAUDE.md is missing from the loaded context.

Secret mission

Add Focus Presets

Common focus lengths should take one click to choose. Add 5, 15, and 25 minute presets while preserving the timer's active-session protections and all seven browser tests.

Clean Up Your Resources

Clean Up Your Resources

Choose whether to keep your local project available, pause the open session, or delete the files. Keeping these resources creates no ongoing cost.

Resources you used:

  • The local project folder ~/focus-sprint-timer containing index.html, style.css, timer.js, app.js, tests.html, tests.js, and CLAUDE.md.
  • An authenticated Claude Code session running in Terminal from ~/focus-sprint-timer.
  • Safari tabs displaying index.html and tests.html.

Keep everything running

No action is needed. Choose this if you want to keep using the timer or continue improving its code.

  • Keep the ~/focus-sprint-timer folder in your home directory.
  • Leave the Claude Code session available for your next change.
  • Keep the Safari tabs available for timer sessions or browser tests.

Your timer stays ready to use without creating any ongoing cost.

Pause - I'll come back to this later

Pausing closes the active windows while preserving every project file. The timer remains ready for a later session.

  • Select Reset in the timer tab to stop an active sprint.
  • Close the Safari tab displaying index.html.
  • Close the Safari tab displaying tests.html.
  • Close the Terminal window from earlier to end the current Claude Code session.

Your workspace is quiet now. Every project file stays ready for your next session.

Delete - I don't want to use this again

Deletion permanently removes every file in the local project folder. Your Claude subscription remains active.

Your Claude Code installation also remains available for other projects.

  • Close the Safari tab displaying index.html.
  • Close the Safari tab displaying tests.html.
  • Close the Terminal window from earlier to end the current Claude Code session.
  • Use macOS search to open Terminal again.
  • Delete the ~/focus-sprint-timer folder by running this command:
rm -rf ~/focus-sprint-timer

What does this command remove?

The command removes the named local folder with all seven project files. The absolute path limits the target to ~/focus-sprint-timer.

The terminal should return to a prompt without printing anything.

  • Close the new Terminal window.
  • Use macOS search to open Finder.
  • Look in your home folder for focus-sprint-timer.

You should no longer see the project folder. That completes the local cleanup.

Still see the project folder?

  • Check that the deletion command targets ~/focus-sprint-timer.
  • Review any permissions message shown in Terminal.
  • Help me diagnose why my local project folder was not deleted.

Nice Work!

Nice Work!

You did it! You built a dependency-free Focus Sprint Timer with Claude Code that supports a complete focus session in Safari.

You've learned how to:

  • Built a complete timer workflow with duration limits, session controls, progress tracking, status updates, and completion feedback.
  • Used Plan mode to define a narrow scope before implementation. Reviewed each proposed diff before approving file changes.
  • Added a browser test harness that protects timer behavior. Stored lasting workflow rules in CLAUDE.md for future Claude Code sessions.
  • Completed the optional Secret Mission by adding reusable 5, 15, and 25 minute presets. Protected active sessions by reusing the timer's existing state rules.

Ready to quiz yourself?