diff --git a/ios/ChronoMindWatch/ChronoMindWatch.entitlements b/ios/ChronoMindWatch/ChronoMindWatch.entitlements
new file mode 100644
index 0000000..cc45b00
--- /dev/null
+++ b/ios/ChronoMindWatch/ChronoMindWatch.entitlements
@@ -0,0 +1,10 @@
+
+
+
+
+ com.apple.security.application-groups
+
+ group.com.chronomind.shared
+
+
+
diff --git a/ios/ChronoMindWatch/ChronoMindWatchApp.swift b/ios/ChronoMindWatch/ChronoMindWatchApp.swift
new file mode 100644
index 0000000..a4b0b29
--- /dev/null
+++ b/ios/ChronoMindWatch/ChronoMindWatchApp.swift
@@ -0,0 +1,15 @@
+// ── ChronoMind Watch App Entry Point ──────────────────────────
+
+import SwiftUI
+
+@main
+struct ChronoMindWatchApp: App {
+ @StateObject private var watchStore = WatchTimerStore()
+
+ var body: some Scene {
+ WindowGroup {
+ WatchContentView()
+ .environmentObject(watchStore)
+ }
+ }
+}
diff --git a/ios/ChronoMindWatch/Complications/WatchComplications.swift b/ios/ChronoMindWatch/Complications/WatchComplications.swift
new file mode 100644
index 0000000..7954deb
--- /dev/null
+++ b/ios/ChronoMindWatch/Complications/WatchComplications.swift
@@ -0,0 +1,242 @@
+// ── Watch Complications ───────────────────────────────────────
+// WidgetKit complications for Apple Watch faces
+
+import SwiftUI
+import WidgetKit
+
+// MARK: - Complication Timeline Provider
+
+struct WatchComplicationProvider: AppIntentTimelineProvider {
+ typealias Entry = WatchComplicationEntry
+ typealias Intent = WatchComplicationIntent
+
+ func placeholder(in context: Context) -> WatchComplicationEntry {
+ WatchComplicationEntry(
+ date: Date(),
+ label: "Standup",
+ targetTime: Date().addingTimeInterval(3600),
+ urgency: .important,
+ hasTimer: true
+ )
+ }
+
+ func snapshot(for configuration: WatchComplicationIntent, in context: Context) async -> WatchComplicationEntry {
+ entryFromSharedData()
+ }
+
+ func timeline(for configuration: WatchComplicationIntent, in context: Context) async -> Timeline {
+ let entry = entryFromSharedData()
+ let now = Date()
+
+ var entries: [WatchComplicationEntry] = []
+ for minuteOffset in stride(from: 0, through: 30, by: 5) {
+ let entryDate = Calendar.current.date(byAdding: .minute, value: minuteOffset, to: now)!
+ entries.append(WatchComplicationEntry(
+ date: entryDate,
+ label: entry.label,
+ targetTime: entry.targetTime,
+ urgency: entry.urgency,
+ hasTimer: entry.hasTimer
+ ))
+ }
+
+ let reloadDate: Date
+ if entry.hasTimer {
+ reloadDate = min(entry.targetTime, now.addingTimeInterval(15 * 60))
+ } else {
+ reloadDate = now.addingTimeInterval(15 * 60)
+ }
+
+ return Timeline(entries: entries, policy: .after(reloadDate))
+ }
+
+ private func entryFromSharedData() -> WatchComplicationEntry {
+ if let timer = SharedTimerDataManager.shared.readNextFiringTimer() {
+ return WatchComplicationEntry(
+ date: Date(),
+ label: timer.label,
+ targetTime: timer.targetTime,
+ urgency: timer.urgency,
+ hasTimer: true
+ )
+ }
+ return WatchComplicationEntry(
+ date: Date(),
+ label: "",
+ targetTime: Date(),
+ urgency: .standard,
+ hasTimer: false
+ )
+ }
+}
+
+// MARK: - Intent
+
+import AppIntents
+
+struct WatchComplicationIntent: WidgetConfigurationIntent {
+ static var title: LocalizedStringResource = "Timer Complication"
+ static var description: IntentDescription = "Shows next timer on watch face"
+}
+
+// MARK: - Entry
+
+struct WatchComplicationEntry: TimelineEntry {
+ let date: Date
+ let label: String
+ let targetTime: Date
+ let urgency: UrgencyLevel
+ let hasTimer: Bool
+}
+
+// MARK: - Circular Complication
+
+struct CircularComplicationView: View {
+ let entry: WatchComplicationEntry
+
+ var body: some View {
+ if entry.hasTimer {
+ ZStack {
+ AccessoryWidgetBackground()
+ VStack(spacing: 0) {
+ Text(entry.targetTime, style: .timer)
+ .font(.system(size: 14, weight: .bold, design: .monospaced))
+ .minimumScaleFactor(0.5)
+ .widgetAccentable()
+ }
+ }
+ } else {
+ ZStack {
+ AccessoryWidgetBackground()
+ Image(systemName: "clock")
+ .font(.system(size: 18))
+ }
+ }
+ }
+}
+
+// MARK: - Rectangular Complication
+
+struct RectangularComplicationView: View {
+ let entry: WatchComplicationEntry
+
+ var body: some View {
+ if entry.hasTimer {
+ HStack {
+ VStack(alignment: .leading, spacing: 2) {
+ Text(entry.label)
+ .font(.system(size: 13, weight: .semibold))
+ .lineLimit(1)
+ .widgetAccentable()
+ Text(entry.targetTime, style: .timer)
+ .font(.system(size: 12, weight: .medium, design: .monospaced))
+ Text(formatTime(entry.targetTime))
+ .font(.system(size: 10))
+ .foregroundStyle(.secondary)
+ }
+ Spacer()
+ }
+ } else {
+ HStack {
+ VStack(alignment: .leading, spacing: 2) {
+ Text("ChronoMind")
+ .font(.system(size: 13, weight: .semibold))
+ Text("No timers")
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ }
+ Spacer()
+ }
+ }
+ }
+}
+
+// MARK: - Inline Complication
+
+struct InlineComplicationView: View {
+ let entry: WatchComplicationEntry
+
+ var body: some View {
+ if entry.hasTimer {
+ HStack(spacing: 4) {
+ Image(systemName: "clock.fill")
+ Text("\(entry.label) in")
+ Text(entry.targetTime, style: .timer)
+ }
+ } else {
+ HStack(spacing: 4) {
+ Image(systemName: "clock")
+ Text("No timers")
+ }
+ }
+ }
+}
+
+// MARK: - Corner Complication
+
+struct CornerComplicationView: View {
+ let entry: WatchComplicationEntry
+
+ var body: some View {
+ if entry.hasTimer {
+ Text(entry.targetTime, style: .timer)
+ .font(.system(size: 12, weight: .bold, design: .monospaced))
+ .widgetAccentable()
+ .widgetLabel {
+ Text(entry.label)
+ }
+ } else {
+ Image(systemName: "clock")
+ .widgetLabel {
+ Text("ChronoMind")
+ }
+ }
+ }
+}
+
+// MARK: - Family Dispatcher
+
+struct WatchComplicationEntryView: View {
+ @Environment(\.widgetFamily) var family
+ let entry: WatchComplicationEntry
+
+ var body: some View {
+ switch family {
+ case .accessoryCircular:
+ CircularComplicationView(entry: entry)
+ case .accessoryRectangular:
+ RectangularComplicationView(entry: entry)
+ case .accessoryInline:
+ InlineComplicationView(entry: entry)
+ case .accessoryCorner:
+ CornerComplicationView(entry: entry)
+ default:
+ CircularComplicationView(entry: entry)
+ }
+ }
+}
+
+// MARK: - Widget Definition
+
+struct WatchTimerComplication: Widget {
+ let kind: String = "WatchTimerComplication"
+
+ var body: some WidgetConfiguration {
+ AppIntentConfiguration(
+ kind: kind,
+ intent: WatchComplicationIntent.self,
+ provider: WatchComplicationProvider()
+ ) { entry in
+ WatchComplicationEntryView(entry: entry)
+ .containerBackground(for: .widget) { }
+ }
+ .configurationDisplayName("Timer")
+ .description("Shows next timer countdown on your watch face")
+ .supportedFamilies([
+ .accessoryCircular,
+ .accessoryRectangular,
+ .accessoryInline,
+ .accessoryCorner,
+ ])
+ }
+}
diff --git a/ios/ChronoMindWatch/Views/WatchContentView.swift b/ios/ChronoMindWatch/Views/WatchContentView.swift
new file mode 100644
index 0000000..4b964a2
--- /dev/null
+++ b/ios/ChronoMindWatch/Views/WatchContentView.swift
@@ -0,0 +1,176 @@
+// ── Watch Content View ────────────────────────────────────────
+// Main navigation for watchOS — timeline + quick timer
+
+import SwiftUI
+
+struct WatchContentView: View {
+ @EnvironmentObject var store: WatchTimerStore
+
+ var body: some View {
+ NavigationStack {
+ if store.timers.isEmpty {
+ emptyState
+ } else {
+ timerList
+ }
+ }
+ .onAppear {
+ store.loadFromSharedData()
+ }
+ }
+
+ // MARK: - Timer List
+
+ private var timerList: some View {
+ List {
+ // Next up section
+ if let next = store.nextFiringTimer {
+ Section {
+ NavigationLink {
+ WatchTimerDetailView(timer: next)
+ } label: {
+ WatchNextUpCard(timer: next, now: store.now)
+ }
+ .listRowBackground(urgencyColor(next.urgency).opacity(0.15))
+ } header: {
+ Text("NEXT UP")
+ .font(.system(size: 10, weight: .semibold))
+ }
+ }
+
+ // Quick timer
+ Section {
+ NavigationLink {
+ WatchQuickTimerView()
+ } label: {
+ Label("Quick Timer", systemImage: "plus.circle.fill")
+ .foregroundStyle(.blue)
+ }
+ }
+
+ // All active timers
+ if store.activeTimers.count > 1 {
+ Section {
+ ForEach(store.activeTimers.filter { $0.id != store.nextFiringTimer?.id }) { timer in
+ NavigationLink {
+ WatchTimerDetailView(timer: timer)
+ } label: {
+ WatchTimerRow(timer: timer, now: store.now)
+ }
+ }
+ } header: {
+ Text("UPCOMING")
+ .font(.system(size: 10, weight: .semibold))
+ }
+ }
+ }
+ .navigationTitle("ChronoMind")
+ }
+
+ // MARK: - Empty State
+
+ private var emptyState: some View {
+ VStack(spacing: 12) {
+ Image(systemName: "clock")
+ .font(.system(size: 36))
+ .foregroundStyle(.secondary)
+
+ Text("No Timers")
+ .font(.system(size: 16, weight: .semibold))
+
+ NavigationLink {
+ WatchQuickTimerView()
+ } label: {
+ Label("Create Timer", systemImage: "plus")
+ }
+ .buttonStyle(.borderedProminent)
+ .tint(.blue)
+ }
+ .navigationTitle("ChronoMind")
+ }
+
+ private func urgencyColor(_ urgency: UrgencyLevel) -> Color {
+ switch urgency {
+ case .critical: return .red
+ case .important: return .orange
+ case .standard: return .yellow
+ case .gentle: return .green
+ case .passive: return .blue
+ }
+ }
+}
+
+// MARK: - Next Up Card
+
+struct WatchNextUpCard: View {
+ let timer: TimerSnapshot
+ let now: Date
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 4) {
+ Text(timer.label)
+ .font(.system(size: 14, weight: .semibold))
+ .lineLimit(1)
+
+ Text(timer.targetTime, style: .timer)
+ .font(.system(size: 24, weight: .bold, design: .monospaced))
+ .foregroundStyle(urgencyColor(timer.urgency))
+
+ Text(formatTime(timer.targetTime))
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ }
+ .padding(.vertical, 4)
+ }
+
+ private func urgencyColor(_ urgency: UrgencyLevel) -> Color {
+ switch urgency {
+ case .critical: return .red
+ case .important: return .orange
+ case .standard: return .yellow
+ case .gentle: return .green
+ case .passive: return .blue
+ }
+ }
+}
+
+// MARK: - Timer Row
+
+struct WatchTimerRow: View {
+ let timer: TimerSnapshot
+ let now: Date
+
+ var body: some View {
+ HStack {
+ Circle()
+ .fill(urgencyColor(timer.urgency))
+ .frame(width: 6, height: 6)
+
+ VStack(alignment: .leading, spacing: 2) {
+ Text(timer.label)
+ .font(.system(size: 13, weight: .medium))
+ .lineLimit(1)
+ Text(formatTime(timer.targetTime))
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ }
+
+ Spacer()
+
+ Text(timer.targetTime, style: .timer)
+ .font(.system(size: 13, weight: .semibold, design: .monospaced))
+ .foregroundStyle(urgencyColor(timer.urgency))
+ .multilineTextAlignment(.trailing)
+ }
+ }
+
+ private func urgencyColor(_ urgency: UrgencyLevel) -> Color {
+ switch urgency {
+ case .critical: return .red
+ case .important: return .orange
+ case .standard: return .yellow
+ case .gentle: return .green
+ case .passive: return .blue
+ }
+ }
+}
diff --git a/ios/ChronoMindWatch/Views/WatchQuickTimerView.swift b/ios/ChronoMindWatch/Views/WatchQuickTimerView.swift
new file mode 100644
index 0000000..1b29948
--- /dev/null
+++ b/ios/ChronoMindWatch/Views/WatchQuickTimerView.swift
@@ -0,0 +1,55 @@
+// ── Watch Quick Timer View ────────────────────────────────────
+// One-tap preset timers for Apple Watch
+
+import SwiftUI
+import WatchKit
+
+struct WatchQuickTimerView: View {
+ @EnvironmentObject var store: WatchTimerStore
+ @Environment(\.dismiss) private var dismiss
+
+ private let presets: [(minutes: Int, label: String, icon: String)] = [
+ (5, "5 min", "5.circle.fill"),
+ (10, "10 min", "10.circle.fill"),
+ (15, "15 min", "15.circle.fill"),
+ (25, "Pomodoro", "target"),
+ (30, "30 min", "30.circle.fill"),
+ (45, "45 min", "45.circle.fill"),
+ (60, "1 hour", "clock.fill"),
+ (90, "1.5 hours", "clock.badge.checkmark"),
+ ]
+
+ var body: some View {
+ ScrollView {
+ LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 8) {
+ ForEach(presets, id: \.minutes) { preset in
+ Button {
+ createTimer(preset)
+ } label: {
+ VStack(spacing: 4) {
+ Image(systemName: preset.icon)
+ .font(.system(size: 18))
+ .foregroundStyle(.blue)
+ Text(preset.label)
+ .font(.system(size: 12, weight: .medium))
+ .lineLimit(1)
+ }
+ .frame(maxWidth: .infinity)
+ .padding(.vertical, 10)
+ .background(.blue.opacity(0.1))
+ .clipShape(RoundedRectangle(cornerRadius: 10))
+ }
+ .buttonStyle(.plain)
+ }
+ }
+ .padding(.horizontal, 4)
+ }
+ .navigationTitle("Quick Timer")
+ .navigationBarTitleDisplayMode(.inline)
+ }
+
+ private func createTimer(_ preset: (minutes: Int, label: String, icon: String)) {
+ store.createQuickTimer(minutes: preset.minutes, label: "\(preset.label) Timer")
+ dismiss()
+ }
+}
diff --git a/ios/ChronoMindWatch/Views/WatchTimerDetailView.swift b/ios/ChronoMindWatch/Views/WatchTimerDetailView.swift
new file mode 100644
index 0000000..6edd7df
--- /dev/null
+++ b/ios/ChronoMindWatch/Views/WatchTimerDetailView.swift
@@ -0,0 +1,98 @@
+// ── Watch Timer Detail View ───────────────────────────────────
+// Full detail view for a timer on Apple Watch
+
+import SwiftUI
+import WatchKit
+
+struct WatchTimerDetailView: View {
+ let timer: TimerSnapshot
+
+ var body: some View {
+ ScrollView {
+ VStack(spacing: 12) {
+ // Urgency badge
+ Text(timer.urgency.rawValue.uppercased())
+ .font(.system(size: 10, weight: .bold))
+ .foregroundStyle(urgencyColor(timer.urgency))
+ .padding(.horizontal, 8)
+ .padding(.vertical, 3)
+ .background(urgencyColor(timer.urgency).opacity(0.2))
+ .clipShape(Capsule())
+
+ // Timer label
+ Text(timer.label)
+ .font(.system(size: 16, weight: .semibold))
+ .multilineTextAlignment(.center)
+
+ // Large countdown
+ Text(timer.targetTime, style: .timer)
+ .font(.system(size: 32, weight: .bold, design: .monospaced))
+ .foregroundStyle(urgencyColor(timer.urgency))
+ .contentTransition(.numericText())
+
+ // Target time
+ HStack(spacing: 4) {
+ Image(systemName: "bell.fill")
+ .font(.system(size: 10))
+ Text(formatTime(timer.targetTime))
+ .font(.system(size: 13, weight: .medium))
+ }
+ .foregroundStyle(.secondary)
+
+ // Pomodoro info
+ if let round = timer.pomodoroCurrentRound,
+ let total = timer.pomodoroTotalRounds {
+ HStack(spacing: 4) {
+ Image(systemName: "target")
+ .font(.system(size: 10))
+ Text(timer.pomodoroIsBreak == true ? "Break" : "Round \(round)/\(total)")
+ .font(.system(size: 12, weight: .medium))
+ }
+ .foregroundStyle(.secondary)
+ }
+
+ // Cascade progress
+ if timer.totalWarnings > 0 {
+ VStack(spacing: 4) {
+ HStack {
+ Text("Warnings")
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ Spacer()
+ Text("\(timer.firedWarnings)/\(timer.totalWarnings)")
+ .font(.system(size: 11, weight: .medium, design: .monospaced))
+ .foregroundStyle(.secondary)
+ }
+ ProgressView(value: Double(timer.firedWarnings), total: Double(timer.totalWarnings))
+ .tint(urgencyColor(timer.urgency))
+ }
+ .padding(.top, 4)
+ }
+
+ // Snooze info
+ if timer.snoozeCount > 0 {
+ HStack(spacing: 4) {
+ Image(systemName: "zzz")
+ .font(.system(size: 10))
+ Text("Snoozed \(timer.snoozeCount)x")
+ .font(.system(size: 11))
+ }
+ .foregroundStyle(.orange)
+ }
+ }
+ .padding()
+ }
+ .navigationTitle(timer.type.rawValue.capitalized)
+ .navigationBarTitleDisplayMode(.inline)
+ }
+
+ private func urgencyColor(_ urgency: UrgencyLevel) -> Color {
+ switch urgency {
+ case .critical: return .red
+ case .important: return .orange
+ case .standard: return .yellow
+ case .gentle: return .green
+ case .passive: return .blue
+ }
+ }
+}
diff --git a/ios/ChronoMindWatch/WatchTimerStore.swift b/ios/ChronoMindWatch/WatchTimerStore.swift
new file mode 100644
index 0000000..802af6e
--- /dev/null
+++ b/ios/ChronoMindWatch/WatchTimerStore.swift
@@ -0,0 +1,124 @@
+// ── Watch Timer Store ─────────────────────────────────────────
+// Reads timer data from App Group shared by the iOS app
+// Lightweight store for watchOS — read-only from shared data + local quick timers
+
+import Foundation
+import Combine
+import WatchKit
+
+@MainActor
+final class WatchTimerStore: ObservableObject {
+ // MARK: - Published State
+
+ @Published var timers: [TimerSnapshot] = []
+ @Published var now: Date = Date()
+
+ // MARK: - Private
+
+ private var tickTimer: Timer?
+ private let sharedData = SharedTimerDataManager.shared
+
+ // MARK: - Init
+
+ init() {
+ loadFromSharedData()
+ startTicking()
+ }
+
+ deinit {
+ tickTimer?.invalidate()
+ }
+
+ // MARK: - Data Loading
+
+ func loadFromSharedData() {
+ timers = sharedData.readActiveSnapshots()
+ }
+
+ // MARK: - Queries
+
+ var nextFiringTimer: TimerSnapshot? {
+ timers
+ .filter { [.active, .warning].contains($0.state) }
+ .sorted { $0.targetTime < $1.targetTime }
+ .first
+ }
+
+ var activeTimers: [TimerSnapshot] {
+ timers.filter { [.active, .warning, .snoozed, .firing].contains($0.state) }
+ }
+
+ // MARK: - Quick Timer Creation (writes to App Group)
+
+ func createQuickTimer(minutes: Int, label: String) {
+ let now = Date()
+ let targetTime = now.addingTimeInterval(TimeInterval(minutes * 60))
+ let intervals = CascadePreset.minimal.defaultIntervals
+
+ let snapshot = TimerSnapshot(
+ id: UUID().uuidString,
+ label: label,
+ type: .countdown,
+ urgency: .standard,
+ state: .active,
+ targetTime: targetTime,
+ duration: TimeInterval(minutes * 60),
+ startedAt: now,
+ elapsedBeforePause: 0,
+ snoozeCount: 0,
+ category: nil,
+ pomodoroCurrentRound: nil,
+ pomodoroTotalRounds: nil,
+ pomodoroIsBreak: nil,
+ nextWarningTime: intervals.first.map { targetTime.addingTimeInterval(-Double($0) * 60) },
+ totalWarnings: intervals.count,
+ firedWarnings: 0
+ )
+
+ // Add to local list and persist
+ timers.append(snapshot)
+ timers.sort { $0.targetTime < $1.targetTime }
+
+ // Write back to shared data so iOS app picks it up
+ sharedData.writeSnapshots(timers)
+
+ // Haptic confirmation
+ WKInterfaceDevice.current().play(.success)
+ }
+
+ // MARK: - Tick
+
+ private func startTicking() {
+ tickTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
+ Task { @MainActor in
+ self?.tick()
+ }
+ }
+ }
+
+ private func tick() {
+ now = Date()
+
+ // Refresh from shared data every 30 seconds
+ let interval = Int(now.timeIntervalSince1970) % 30
+ if interval == 0 {
+ loadFromSharedData()
+ }
+
+ // Check for fired timers
+ var changed = false
+ for i in timers.indices {
+ if timers[i].state == .active || timers[i].state == .warning {
+ if now >= timers[i].targetTime {
+ // Timer has fired — haptic alert
+ WKInterfaceDevice.current().play(.notification)
+ changed = true
+ }
+ }
+ }
+
+ if changed {
+ loadFromSharedData() // Refresh to get updated states
+ }
+ }
+}