← Commit history
README.md+58−1
@@ -1,3 +1,60 @@ # 5-Minute Timer -A countdown timer that goes green to red, then flashes red and counts negative when time's up.+A countdown timer that starts green, sweeps to red as it runs down, and once it passes zero keeps counting negative while the whole screen flashes red at 1 Hz. Play/pause, reset, and on-the-fly time adjustments. One self-contained HTML file — no build, no server.++![5-Minute Timer](/blob/app/timer/screenshots/hero.png)++## Live Demo++<iframe src="/blob/app/timer/demo/index.html" width="100%" height="560" frameborder="0" style="border-radius: 10px; border: 1px solid #30363d;"></iframe>++Press **Start**, let it run past zero, and watch it flash. Everything is baked in — the demo above is the real app.++## Install++```+adompkg install timer+```++## Quick Start++```bash+timer                  # serve it locally → http://localhost:8741/+TIMER_PORT=9000 timer  # pick a different port+```++Or just open `~/.local/share/adom-timer/index.html` in any browser — it needs no server.++## Screenshots++### Ready — full green at 5:00+![Ready state](/blob/app/timer/screenshots/ready.png)++### Time up — flashing red, counting negative+![Expired state](/blob/app/timer/screenshots/expired.png)++## Features++- 5-minute countdown with a depleting progress ring and large tabular digits.+- Color sweeps green → yellow → red as the remaining time shrinks.+- Past zero it counts negative (`-0:01`, `-0:02`, …) and the whole background strobes red at **1 Hz** while running.+- Pause stops the strobe (digits stay red) for a calm, readable "time's up" state.+- **Play / Pause** and **Reset** buttons (Space and R keyboard shortcuts).+- **−60s / −30s / −10s / +10s / +30s / +60s** adjust buttons that work whether running or paused — add time to climb back into the green.+- Wall-clock-accurate timing (`performance.now()`), so it stays correct even when the tab is backgrounded.+- Fully self-contained: a single static HTML file, zero dependencies.++## Dependencies++This app has **zero** adompkg dependencies. It is one self-contained HTML file with no external libraries, fonts, or services.++## Talk to the AI++- **Open / run:** "open the timer", "start a 5 minute timer", "serve the countdown timer"+- **Use:** "reset the timer", "add 30 seconds", "pause it"++## Skills++| Skill | Purpose |+|---|---|+| `timer` | Describes the app, how to run it (`timer` launcher or open the HTML directly), the controls, and the green→red→flash behavior. |
SKILL.mdadded+36
@@ -0,0 +1,36 @@+---+name: timer+description: "A 5-minute countdown timer web app. Shifts green to red as it expires, then flashes red at 1 Hz and counts negative once time is up. Play/pause, reset, and +/- 10/30/60s adjust buttons. Use when the user wants a visual countdown timer, a meeting/speaking timer, a stand-up timer, or asks to open/serve the timer. Trigger words: timer, countdown, 5 minute timer, start a timer, stopwatch countdown, meeting timer, open the timer."+---++# 5-Minute Timer++A self-contained single-file web app: a 5-minute countdown that is green at the+start, sweeps through yellow to red as it runs down, and once it passes zero it+keeps counting negative while the whole background flashes red at 1 Hz.++## Run it++```bash+timer                 # serves the app on http://localhost:8741/+TIMER_PORT=9000 timer # use a different port+```++Or open `~/.local/share/adom-timer/index.html` directly in any browser — there+is no server requirement; it is pure HTML/CSS/JS.++## Controls++- **Start / Pause** — toggle the countdown (keyboard: Space).+- **Reset** — back to 5:00 (keyboard: R).+- **−60s / −30s / −10s / +10s / +30s / +60s** — adjust the remaining time on+  the fly, whether running or paused. You can add time after it's gone negative+  to bring it back into the green.++## Behavior++- Color sweeps green (5:00) → yellow → red (0:00) following the remaining time.+- Past zero it shows `-0:01`, `-0:02`, … with the page background strobing red+  at 1 Hz **while running**; pausing stops the strobe (digits stay red).+- Timing is wall-clock accurate (uses `performance.now()`), so it stays correct+  even if the browser tab is backgrounded.
demo/index.htmladded+219
@@ -0,0 +1,219 @@+<!DOCTYPE html>+<html lang="en">+<head>+<meta charset="UTF-8">+<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">+<title>5-Minute Timer</title>+<style>+  :root {+    --bg: #0d1117;+    --panel: #161b22;+    --text: #e6edf3;+    --muted: #8b949e;+    --border: #30363d;+  }+  * { box-sizing: border-box; margin: 0; padding: 0; -webkit-user-select: none; user-select: none; }+  html, body { height: 100%; }+  body {+    background: radial-gradient(ellipse at center, #11161f 0%, var(--bg) 70%);+    color: var(--text);+    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;+    display: flex; flex-direction: column; align-items: center; justify-content: center;+    min-height: 100%; gap: 40px; padding: 24px;+  }++  .timer { position: relative; width: min(78vw, 420px); aspect-ratio: 1; display: grid; place-items: center; }+  .ring { position: absolute; inset: 0; width: 100%; height: 100%; transform: rotate(-90deg); }+  .ring .track { fill: none; stroke: var(--border); stroke-width: 8; }+  .ring .progress {+    fill: none; stroke-width: 12; stroke-linecap: round;+    transition: stroke 0.25s linear;+    filter: drop-shadow(0 0 10px currentColor);+  }++  .readout { display: flex; flex-direction: column; align-items: center; gap: 6px; }+  .time {+    font-variant-numeric: tabular-nums;+    font-family: "SF Mono", "JetBrains Mono", ui-monospace, Menlo, Consolas, monospace;+    font-size: clamp(48px, 17vw, 96px); font-weight: 700; line-height: 1;+    letter-spacing: 1px; transition: color 0.25s linear;+  }+  .label { font-size: 13px; letter-spacing: 2px; text-transform: uppercase; color: var(--muted); }++  /* Flash the WHOLE background red when expired (past zero) */+  body.expired { animation: bgFlash 1s ease-in-out infinite; }   /* 1 Hz */+  @keyframes bgFlash {+    0%, 100% { background: var(--bg); }+    50%      { background: #c01f28; }+  }++  .controls { display: flex; gap: 14px; }+  button {+    font-family: inherit; font-size: 15px; font-weight: 600;+    color: var(--text); background: var(--panel);+    border: 1px solid var(--border); border-radius: 10px;+    padding: 12px 22px; min-width: 110px; cursor: pointer;+    transition: background 0.15s, border-color 0.15s, transform 0.05s;+    display: inline-flex; align-items: center; justify-content: center; gap: 8px;+  }+  button:hover { background: #1f2630; border-color: #484f58; }+  button:active { transform: translateY(1px); }+  button.primary { background: #1f6feb22; border-color: #1f6feb88; color: #cfe2ff; }+  button.primary:hover { background: #1f6feb33; }+  button svg { width: 16px; height: 16px; fill: currentColor; }++  .adjust { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; max-width: 430px; }+  .adj { min-width: 58px; padding: 9px 12px; font-size: 14px; }+  .adj[data-delta^="-"] { color: #ffc2c2; border-color: #5a2a2a; }+  .adj[data-delta^="-"]:hover { background: #261313; border-color: #7a3a3a; }+  .adj:not([data-delta^="-"]) { color: #bdf0cd; border-color: #2c5a3e; }+  .adj:not([data-delta^="-"]):hover { background: #16271c; border-color: #3c7a52; }+</style>+</head>+<body>+  <div class="timer" id="timer">+    <svg class="ring" viewBox="0 0 240 240">+      <circle class="track" cx="120" cy="120" r="110"></circle>+      <circle class="progress" id="progress" cx="120" cy="120" r="110"></circle>+    </svg>+    <div class="readout">+      <div class="time" id="time">5:00</div>+      <div class="label" id="label">ready</div>+    </div>+  </div>++  <div class="controls">+    <button class="primary" id="playBtn" aria-label="Play or pause">+      <svg id="playIcon" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>+      <span id="playLabel">Start</span>+    </button>+    <button id="resetBtn" aria-label="Reset">+      <svg viewBox="0 0 24 24"><path d="M12 5V1L7 6l5 5V7a5 5 0 1 1-5 5H5a7 7 0 1 0 7-7z"/></svg>+      <span>Reset</span>+    </button>+  </div>++  <div class="adjust" id="adjust">+    <button class="adj" data-delta="-60">&minus;60s</button>+    <button class="adj" data-delta="-30">&minus;30s</button>+    <button class="adj" data-delta="-10">&minus;10s</button>+    <button class="adj" data-delta="10">+10s</button>+    <button class="adj" data-delta="30">+30s</button>+    <button class="adj" data-delta="60">+60s</button>+  </div>++<script>+(function () {+  const TOTAL = 5 * 60;            // 5 minutes, seconds+  const R = 110;+  const CIRC = 2 * Math.PI * R;++  const $timer = document.getElementById('timer');+  const $time = document.getElementById('time');+  const $label = document.getElementById('label');+  const $progress = document.getElementById('progress');+  const $playBtn = document.getElementById('playBtn');+  const $playIcon = document.getElementById('playIcon');+  const $playLabel = document.getElementById('playLabel');+  const $resetBtn = document.getElementById('resetBtn');++  $progress.style.strokeDasharray = CIRC;++  const ICON_PLAY = 'M8 5v14l11-7z';+  const ICON_PAUSE = 'M6 5h4v14H6zM14 5h4v14h-4z';++  let remaining = TOTAL;   // seconds, may go negative+  let running = false;+  let endAt = 0;           // performance.now()/1000 + remaining, while running++  function now() { return performance.now() / 1000; }++  // green (120°) at full time -> red (0°) at zero+  function colorFor(r) {+    if (r > 0) {+      const hue = 120 * Math.max(0, Math.min(1, r / TOTAL));+      return `hsl(${hue.toFixed(1)}, 85%, 55%)`;+    }+    return 'hsl(0, 90%, 58%)';+  }++  function fmt(r) {+    let neg = false, secs;+    if (r > 0) { secs = Math.ceil(r - 1e-9); }+    else { secs = Math.ceil(-r - 1e-9); neg = secs > 0; }+    const m = Math.floor(secs / 60);+    const s = secs % 60;+    return (neg ? '-' : '') + m + ':' + String(s).padStart(2, '0');+  }++  function render() {+    const expired = remaining <= 0;+    const flashing = expired && running;   // only strobe while actually counting past 0+    const color = colorFor(remaining);++    $time.textContent = fmt(remaining);+    $time.style.color = flashing ? '#ffffff' : color;   // white over the strobe; red when paused-expired+    $progress.style.stroke = color;++    // ring: full at TOTAL, empty at <=0+    const frac = Math.max(0, Math.min(1, remaining / TOTAL));+    $progress.style.strokeDashoffset = CIRC * (1 - frac);++    $timer.classList.toggle('expired', expired);+    document.body.classList.toggle('expired', flashing);   // strobe stops when paused+    $label.textContent = expired ? 'time up' : (running ? 'running' : (remaining === TOTAL ? 'ready' : 'paused'));+  }++  function tick() {+    if (running) remaining = endAt - now();+    render();+    requestAnimationFrame(tick);+  }++  function setPlayingUI(isRunning) {+    $playIcon.querySelector('path').setAttribute('d', isRunning ? ICON_PAUSE : ICON_PLAY);+    $playLabel.textContent = isRunning ? 'Pause' : (remaining === TOTAL ? 'Start' : 'Resume');+  }++  function play() {+    if (running) return;+    endAt = now() + remaining;+    running = true;+    setPlayingUI(true);+  }+  function pause() {+    if (!running) return;+    remaining = endAt - now();+    running = false;+    setPlayingUI(false);+  }+  function toggle() { running ? pause() : play(); }+  function reset() {+    running = false;+    remaining = TOTAL;+    setPlayingUI(false);+    render();+  }+  function adjust(delta) {+    if (running) { endAt += delta; remaining = endAt - now(); }+    else { remaining += delta; }+    setPlayingUI(running);   // refresh Start/Resume label+    render();+  }++  $playBtn.addEventListener('click', toggle);+  $resetBtn.addEventListener('click', reset);+  document.querySelectorAll('.adj').forEach(b => {+    b.addEventListener('click', () => adjust(parseInt(b.dataset.delta, 10)));+  });+  document.addEventListener('keydown', (e) => {+    if (e.code === 'Space') { e.preventDefault(); toggle(); }+    else if (e.key === 'r' || e.key === 'R') { reset(); }+  });++  setPlayingUI(false);+  requestAnimationFrame(tick);+})();+</script>+</body>+</html>
index.htmladded+219
@@ -0,0 +1,219 @@+<!DOCTYPE html>+<html lang="en">+<head>+<meta charset="UTF-8">+<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">+<title>5-Minute Timer</title>+<style>+  :root {+    --bg: #0d1117;+    --panel: #161b22;+    --text: #e6edf3;+    --muted: #8b949e;+    --border: #30363d;+  }+  * { box-sizing: border-box; margin: 0; padding: 0; -webkit-user-select: none; user-select: none; }+  html, body { height: 100%; }+  body {+    background: radial-gradient(ellipse at center, #11161f 0%, var(--bg) 70%);+    color: var(--text);+    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;+    display: flex; flex-direction: column; align-items: center; justify-content: center;+    min-height: 100%; gap: 40px; padding: 24px;+  }++  .timer { position: relative; width: min(78vw, 420px); aspect-ratio: 1; display: grid; place-items: center; }+  .ring { position: absolute; inset: 0; width: 100%; height: 100%; transform: rotate(-90deg); }+  .ring .track { fill: none; stroke: var(--border); stroke-width: 8; }+  .ring .progress {+    fill: none; stroke-width: 12; stroke-linecap: round;+    transition: stroke 0.25s linear;+    filter: drop-shadow(0 0 10px currentColor);+  }++  .readout { display: flex; flex-direction: column; align-items: center; gap: 6px; }+  .time {+    font-variant-numeric: tabular-nums;+    font-family: "SF Mono", "JetBrains Mono", ui-monospace, Menlo, Consolas, monospace;+    font-size: clamp(48px, 17vw, 96px); font-weight: 700; line-height: 1;+    letter-spacing: 1px; transition: color 0.25s linear;+  }+  .label { font-size: 13px; letter-spacing: 2px; text-transform: uppercase; color: var(--muted); }++  /* Flash the WHOLE background red when expired (past zero) */+  body.expired { animation: bgFlash 1s ease-in-out infinite; }   /* 1 Hz */+  @keyframes bgFlash {+    0%, 100% { background: var(--bg); }+    50%      { background: #c01f28; }+  }++  .controls { display: flex; gap: 14px; }+  button {+    font-family: inherit; font-size: 15px; font-weight: 600;+    color: var(--text); background: var(--panel);+    border: 1px solid var(--border); border-radius: 10px;+    padding: 12px 22px; min-width: 110px; cursor: pointer;+    transition: background 0.15s, border-color 0.15s, transform 0.05s;+    display: inline-flex; align-items: center; justify-content: center; gap: 8px;+  }+  button:hover { background: #1f2630; border-color: #484f58; }+  button:active { transform: translateY(1px); }+  button.primary { background: #1f6feb22; border-color: #1f6feb88; color: #cfe2ff; }+  button.primary:hover { background: #1f6feb33; }+  button svg { width: 16px; height: 16px; fill: currentColor; }++  .adjust { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; max-width: 430px; }+  .adj { min-width: 58px; padding: 9px 12px; font-size: 14px; }+  .adj[data-delta^="-"] { color: #ffc2c2; border-color: #5a2a2a; }+  .adj[data-delta^="-"]:hover { background: #261313; border-color: #7a3a3a; }+  .adj:not([data-delta^="-"]) { color: #bdf0cd; border-color: #2c5a3e; }+  .adj:not([data-delta^="-"]):hover { background: #16271c; border-color: #3c7a52; }+</style>+</head>+<body>+  <div class="timer" id="timer">+    <svg class="ring" viewBox="0 0 240 240">+      <circle class="track" cx="120" cy="120" r="110"></circle>+      <circle class="progress" id="progress" cx="120" cy="120" r="110"></circle>+    </svg>+    <div class="readout">+      <div class="time" id="time">5:00</div>+      <div class="label" id="label">ready</div>+    </div>+  </div>++  <div class="controls">+    <button class="primary" id="playBtn" aria-label="Play or pause">+      <svg id="playIcon" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>+      <span id="playLabel">Start</span>+    </button>+    <button id="resetBtn" aria-label="Reset">+      <svg viewBox="0 0 24 24"><path d="M12 5V1L7 6l5 5V7a5 5 0 1 1-5 5H5a7 7 0 1 0 7-7z"/></svg>+      <span>Reset</span>+    </button>+  </div>++  <div class="adjust" id="adjust">+    <button class="adj" data-delta="-60">&minus;60s</button>+    <button class="adj" data-delta="-30">&minus;30s</button>+    <button class="adj" data-delta="-10">&minus;10s</button>+    <button class="adj" data-delta="10">+10s</button>+    <button class="adj" data-delta="30">+30s</button>+    <button class="adj" data-delta="60">+60s</button>+  </div>++<script>+(function () {+  const TOTAL = 5 * 60;            // 5 minutes, seconds+  const R = 110;+  const CIRC = 2 * Math.PI * R;++  const $timer = document.getElementById('timer');+  const $time = document.getElementById('time');+  const $label = document.getElementById('label');+  const $progress = document.getElementById('progress');+  const $playBtn = document.getElementById('playBtn');+  const $playIcon = document.getElementById('playIcon');+  const $playLabel = document.getElementById('playLabel');+  const $resetBtn = document.getElementById('resetBtn');++  $progress.style.strokeDasharray = CIRC;++  const ICON_PLAY = 'M8 5v14l11-7z';+  const ICON_PAUSE = 'M6 5h4v14H6zM14 5h4v14h-4z';++  let remaining = TOTAL;   // seconds, may go negative+  let running = false;+  let endAt = 0;           // performance.now()/1000 + remaining, while running++  function now() { return performance.now() / 1000; }++  // green (120°) at full time -> red (0°) at zero+  function colorFor(r) {+    if (r > 0) {+      const hue = 120 * Math.max(0, Math.min(1, r / TOTAL));+      return `hsl(${hue.toFixed(1)}, 85%, 55%)`;+    }+    return 'hsl(0, 90%, 58%)';+  }++  function fmt(r) {+    let neg = false, secs;+    if (r > 0) { secs = Math.ceil(r - 1e-9); }+    else { secs = Math.ceil(-r - 1e-9); neg = secs > 0; }+    const m = Math.floor(secs / 60);+    const s = secs % 60;+    return (neg ? '-' : '') + m + ':' + String(s).padStart(2, '0');+  }++  function render() {+    const expired = remaining <= 0;+    const flashing = expired && running;   // only strobe while actually counting past 0+    const color = colorFor(remaining);++    $time.textContent = fmt(remaining);+    $time.style.color = flashing ? '#ffffff' : color;   // white over the strobe; red when paused-expired+    $progress.style.stroke = color;++    // ring: full at TOTAL, empty at <=0+    const frac = Math.max(0, Math.min(1, remaining / TOTAL));+    $progress.style.strokeDashoffset = CIRC * (1 - frac);++    $timer.classList.toggle('expired', expired);+    document.body.classList.toggle('expired', flashing);   // strobe stops when paused+    $label.textContent = expired ? 'time up' : (running ? 'running' : (remaining === TOTAL ? 'ready' : 'paused'));+  }++  function tick() {+    if (running) remaining = endAt - now();+    render();+    requestAnimationFrame(tick);+  }++  function setPlayingUI(isRunning) {+    $playIcon.querySelector('path').setAttribute('d', isRunning ? ICON_PAUSE : ICON_PLAY);+    $playLabel.textContent = isRunning ? 'Pause' : (remaining === TOTAL ? 'Start' : 'Resume');+  }++  function play() {+    if (running) return;+    endAt = now() + remaining;+    running = true;+    setPlayingUI(true);+  }+  function pause() {+    if (!running) return;+    remaining = endAt - now();+    running = false;+    setPlayingUI(false);+  }+  function toggle() { running ? pause() : play(); }+  function reset() {+    running = false;+    remaining = TOTAL;+    setPlayingUI(false);+    render();+  }+  function adjust(delta) {+    if (running) { endAt += delta; remaining = endAt - now(); }+    else { remaining += delta; }+    setPlayingUI(running);   // refresh Start/Resume label+    render();+  }++  $playBtn.addEventListener('click', toggle);+  $resetBtn.addEventListener('click', reset);+  document.querySelectorAll('.adj').forEach(b => {+    b.addEventListener('click', () => adjust(parseInt(b.dataset.delta, 10)));+  });+  document.addEventListener('keydown', (e) => {+    if (e.code === 'Space') { e.preventDefault(); toggle(); }+    else if (e.key === 'r' || e.key === 'R') { reset(); }+  });++  setPlayingUI(false);+  requestAnimationFrame(tick);+})();+</script>+</body>+</html>
install.shadded+34
@@ -0,0 +1,34 @@+#!/bin/bash+# Install the 5-Minute Timer: stage the self-contained web app + a `timer` launcher.+set -e++APP_DIR="$HOME/.local/share/adom-timer"+BIN_DIR="$HOME/.local/bin"+MOD_DIR="$(cd "$(dirname "$0")" && pwd)"++mkdir -p "$APP_DIR" "$BIN_DIR"++# The timer is a single self-contained HTML file — no build, no server deps.+cp "$MOD_DIR/index.html" "$APP_DIR/index.html"++# Launcher: serve the app on a free port and print the URL.+cat > "$BIN_DIR/timer" <<'EOF'+#!/bin/bash+# Serve the 5-Minute Timer locally and print its URL.+APP_DIR="$HOME/.local/share/adom-timer"+PORT="${TIMER_PORT:-8741}"+echo "Serving 5-Minute Timer at http://localhost:$PORT/"+echo "(Ctrl-C to stop. Override the port with TIMER_PORT=NNNN timer.)"+exec python3 -m http.server "$PORT" --directory "$APP_DIR"+EOF+chmod +x "$BIN_DIR/timer"++# Install the skill so Claude Code knows about the app.+SKILL_DIR="$HOME/.claude/skills/timer"+mkdir -p "$SKILL_DIR"+cp "$MOD_DIR/SKILL.md" "$SKILL_DIR/SKILL.md"++echo "OK: 5-Minute Timer installed."+echo "  - Run:  timer            (serves it locally, opens on a port)"+echo "  - File: $APP_DIR/index.html  (open directly in any browser)"+case ":$PATH:" in *":$BIN_DIR:"*) ;; *) echo "  note: add $BIN_DIR to PATH to use the 'timer' command." ;; esac
package.jsonadded+23
@@ -0,0 +1,23 @@+{+  "slug": "timer",+  "version": "0.1.0",+  "type": "app",+  "description": "A 5-minute countdown timer that shifts green to red as it expires, then flashes red at 1 Hz and counts negative. Play/pause, reset, and +/- 10/30/60s adjust buttons. Self-contained single-file web app.",+  "dependencies": {},+  "files": [+    "SKILL.md",+    "README.md",+    "page.json",+    "install.sh",+    "uninstall.sh",+    "index.html",+    "bin/**",+    "demo/**",+    "skills/**",+    "screenshots/**"+  ],+  "scripts": {+    "install": "./install.sh",+    "uninstall": "./uninstall.sh"+  }+}
page.json+5−26
@@ -1,32 +1,11 @@ {-  "schema_version": 1,   "type": "app",   "slug": "timer",   "title": "5-Minute Timer",   "brief": "A countdown timer that goes green to red, then flashes red and counts negative when time's up.",   "version": "0.1.0",-  "visibility": {-    "public": true-  },-  "install": {-    "binary_name": "timer",-    "install_dir": "",-    "install_hint": "",-    "version_cmd": ""-  },-  "author": {-    "id": "695820315b5f1e4db2fcf602",-    "name": "Kyle Bergstedt",-    "email": "[email protected]"-  },-  "tags": [],-  "license": "proprietary",-  "hero": null,-  "sample_prompts": [],-  "discovery_triggers": [],-  "discovery_pitch": null,-  "metadata": {},-  "created_at": "2026-06-29T19:33:38.282Z",-  "updated_at": "2026-06-29T19:33:38.282Z",-  "skills": []-}\ No newline at end of file+  "visibility": {"public": true},+  "hero": {"type": "image", "path": "screenshots/hero.png"},+  "install": {"binary_name": "timer"},+  "metadata": {"platform": "any"}+}
screenshots/expired.pngadded
⋯ 1 unchanged line ⋯
screenshots/hero.pngadded
⋯ 1 unchanged line ⋯
screenshots/ready.pngadded
⋯ 1 unchanged line ⋯
uninstall.shadded+7
@@ -0,0 +1,7 @@+#!/bin/bash+# Remove the 5-Minute Timer app, launcher, and skill.+set -e+rm -rf "$HOME/.local/share/adom-timer"+rm -f "$HOME/.local/bin/timer"+rm -rf "$HOME/.claude/skills/timer"+echo "OK: 5-Minute Timer uninstalled."