diff --git a/ios/ChronoMindWidgets/ChronoMindWidgetBundle.swift b/ios/ChronoMindWidgets/ChronoMindWidgetBundle.swift
new file mode 100644
index 0000000..70f83b2
--- /dev/null
+++ b/ios/ChronoMindWidgets/ChronoMindWidgetBundle.swift
@@ -0,0 +1,15 @@
+// ── Widget Bundle ─────────────────────────────────────────────
+// WidgetKit extension containing all ChronoMind widgets + Live Activity
+
+import SwiftUI
+import WidgetKit
+
+@main
+struct ChronoMindWidgetBundle: WidgetBundle {
+ var body: some Widget {
+ NextTimerWidget()
+ TimerListWidget()
+ NextTimerLockScreenWidget()
+ TimerLiveActivity()
+ }
+}
diff --git a/ios/ChronoMindWidgets/ChronoMindWidgets.entitlements b/ios/ChronoMindWidgets/ChronoMindWidgets.entitlements
new file mode 100644
index 0000000..cc45b00
--- /dev/null
+++ b/ios/ChronoMindWidgets/ChronoMindWidgets.entitlements
@@ -0,0 +1,10 @@
+
+
+
+
+ com.apple.security.application-groups
+
+ group.com.chronomind.shared
+
+
+
diff --git a/ios/ChronoMindWidgets/NextTimerLockScreenWidget.swift b/ios/ChronoMindWidgets/NextTimerLockScreenWidget.swift
new file mode 100644
index 0000000..eb99979
--- /dev/null
+++ b/ios/ChronoMindWidgets/NextTimerLockScreenWidget.swift
@@ -0,0 +1,215 @@
+// ── Lock Screen Widget ────────────────────────────────────────
+// Lock screen inline/circular/rectangular widgets for next timer
+
+import SwiftUI
+import WidgetKit
+import AppIntents
+
+// MARK: - Timeline Provider
+
+struct LockScreenTimerProvider: AppIntentTimelineProvider {
+ typealias Entry = LockScreenTimerEntry
+ typealias Intent = LockScreenTimerIntent
+
+ func placeholder(in context: Context) -> LockScreenTimerEntry {
+ LockScreenTimerEntry(
+ date: Date(),
+ label: "Standup",
+ targetTime: Date().addingTimeInterval(3600),
+ urgency: .important,
+ hasTimer: true
+ )
+ }
+
+ func snapshot(for configuration: LockScreenTimerIntent, in context: Context) async -> LockScreenTimerEntry {
+ entryFromSharedData()
+ }
+
+ func timeline(for configuration: LockScreenTimerIntent, in context: Context) async -> Timeline {
+ let entry = entryFromSharedData()
+ let now = Date()
+
+ var entries: [LockScreenTimerEntry] = []
+ for minuteOffset in stride(from: 0, through: 30, by: 5) {
+ let entryDate = Calendar.current.date(byAdding: .minute, value: minuteOffset, to: now)!
+ entries.append(LockScreenTimerEntry(
+ 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(30 * 60))
+ } else {
+ reloadDate = now.addingTimeInterval(30 * 60)
+ }
+
+ return Timeline(entries: entries, policy: .after(reloadDate))
+ }
+
+ private func entryFromSharedData() -> LockScreenTimerEntry {
+ if let timer = SharedTimerDataManager.shared.readNextFiringTimer() {
+ return LockScreenTimerEntry(
+ date: Date(),
+ label: timer.label,
+ targetTime: timer.targetTime,
+ urgency: timer.urgency,
+ hasTimer: true
+ )
+ }
+ return LockScreenTimerEntry(
+ date: Date(),
+ label: "",
+ targetTime: Date(),
+ urgency: .standard,
+ hasTimer: false
+ )
+ }
+}
+
+// MARK: - Intent
+
+struct LockScreenTimerIntent: WidgetConfigurationIntent {
+ static var title: LocalizedStringResource = "Lock Screen Timer"
+ static var description: IntentDescription = "Shows next timer countdown on lock screen"
+}
+
+// MARK: - Entry
+
+struct LockScreenTimerEntry: TimelineEntry {
+ let date: Date
+ let label: String
+ let targetTime: Date
+ let urgency: UrgencyLevel
+ let hasTimer: Bool
+}
+
+// MARK: - Widget Views
+
+struct LockScreenInlineView: View {
+ let entry: LockScreenTimerEntry
+
+ var body: some View {
+ if entry.hasTimer {
+ HStack(spacing: 4) {
+ Image(systemName: "clock.fill")
+ Text(entry.label)
+ Text(entry.targetTime, style: .timer)
+ }
+ } else {
+ HStack(spacing: 4) {
+ Image(systemName: "clock")
+ Text("No timers")
+ }
+ }
+ }
+}
+
+struct LockScreenCircularView: View {
+ let entry: LockScreenTimerEntry
+
+ var body: some View {
+ if entry.hasTimer {
+ ZStack {
+ // Progress ring based on remaining time
+ 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: 20))
+ }
+ }
+ }
+}
+
+struct LockScreenRectangularView: View {
+ let entry: LockScreenTimerEntry
+
+ var body: some View {
+ if entry.hasTimer {
+ HStack(spacing: 8) {
+ VStack(alignment: .leading, spacing: 2) {
+ Text(entry.label)
+ .font(.system(size: 14, 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: 14, weight: .semibold))
+ Text("No active timers")
+ .font(.system(size: 12))
+ .foregroundStyle(.secondary)
+ }
+ Spacer()
+ }
+ }
+ }
+}
+
+// MARK: - Widget Definition
+
+// MARK: - Family Dispatcher
+
+struct LockScreenWidgetEntryView: View {
+ @Environment(\.widgetFamily) var family
+ let entry: LockScreenTimerEntry
+
+ var body: some View {
+ switch family {
+ case .accessoryInline:
+ LockScreenInlineView(entry: entry)
+ case .accessoryCircular:
+ LockScreenCircularView(entry: entry)
+ case .accessoryRectangular:
+ LockScreenRectangularView(entry: entry)
+ default:
+ LockScreenRectangularView(entry: entry)
+ }
+ }
+}
+
+// MARK: - Widget Definition
+
+struct NextTimerLockScreenWidget: Widget {
+ let kind: String = "NextTimerLockScreenWidget"
+
+ var body: some WidgetConfiguration {
+ AppIntentConfiguration(
+ kind: kind,
+ intent: LockScreenTimerIntent.self,
+ provider: LockScreenTimerProvider()
+ ) { entry in
+ LockScreenWidgetEntryView(entry: entry)
+ .containerBackground(for: .widget) { }
+ }
+ .configurationDisplayName("Timer Countdown")
+ .description("Shows next timer countdown on your lock screen")
+ .supportedFamilies([
+ .accessoryInline,
+ .accessoryCircular,
+ .accessoryRectangular,
+ ])
+ }
+}
diff --git a/ios/ChronoMindWidgets/NextTimerWidget.swift b/ios/ChronoMindWidgets/NextTimerWidget.swift
new file mode 100644
index 0000000..2731dc9
--- /dev/null
+++ b/ios/ChronoMindWidgets/NextTimerWidget.swift
@@ -0,0 +1,183 @@
+// ── Next Timer Widget (Small) ─────────────────────────────────
+// Home screen small widget showing next timer + countdown
+
+import SwiftUI
+import WidgetKit
+
+// MARK: - Timeline Provider
+
+struct NextTimerProvider: AppIntentTimelineProvider {
+ typealias Entry = NextTimerEntry
+ typealias Intent = NextTimerIntent
+
+ func placeholder(in context: Context) -> NextTimerEntry {
+ NextTimerEntry(
+ date: Date(),
+ timer: TimerSnapshot(
+ id: "placeholder",
+ label: "Team Standup",
+ type: .alarm,
+ urgency: .important,
+ state: .active,
+ targetTime: Date().addingTimeInterval(3600),
+ duration: 3600,
+ startedAt: Date(),
+ elapsedBeforePause: 0,
+ snoozeCount: 0,
+ category: nil,
+ pomodoroCurrentRound: nil,
+ pomodoroTotalRounds: nil,
+ pomodoroIsBreak: nil,
+ nextWarningTime: Date().addingTimeInterval(1800),
+ totalWarnings: 3,
+ firedWarnings: 1
+ )
+ )
+ }
+
+ func snapshot(for configuration: NextTimerIntent, in context: Context) async -> NextTimerEntry {
+ let timer = SharedTimerDataManager.shared.readNextFiringTimer()
+ return NextTimerEntry(date: Date(), timer: timer)
+ }
+
+ func timeline(for configuration: NextTimerIntent, in context: Context) async -> Timeline {
+ let timer = SharedTimerDataManager.shared.readNextFiringTimer()
+ let now = Date()
+
+ // Create entries every 5 minutes for the next 30 minutes
+ var entries: [NextTimerEntry] = []
+ for minuteOffset in stride(from: 0, through: 30, by: 5) {
+ let entryDate = Calendar.current.date(byAdding: .minute, value: minuteOffset, to: now)!
+ entries.append(NextTimerEntry(date: entryDate, timer: timer))
+ }
+
+ // Reload after 30 minutes or when timer fires
+ let reloadDate: Date
+ if let timer = timer {
+ reloadDate = min(timer.targetTime, now.addingTimeInterval(30 * 60))
+ } else {
+ reloadDate = now.addingTimeInterval(30 * 60)
+ }
+
+ return Timeline(entries: entries, policy: .after(reloadDate))
+ }
+}
+
+// MARK: - Intent (no config needed, just a placeholder for AppIntentTimelineProvider)
+
+import AppIntents
+
+struct NextTimerIntent: WidgetConfigurationIntent {
+ static var title: LocalizedStringResource = "Next Timer"
+ static var description: IntentDescription = "Shows your next upcoming timer"
+}
+
+// MARK: - Entry
+
+struct NextTimerEntry: TimelineEntry {
+ let date: Date
+ let timer: TimerSnapshot?
+}
+
+// MARK: - Widget View
+
+struct NextTimerWidgetView: View {
+ @Environment(\.widgetFamily) var family
+ let entry: NextTimerEntry
+
+ var body: some View {
+ if let timer = entry.timer {
+ timerContent(timer)
+ } else {
+ emptyContent
+ }
+ }
+
+ @ViewBuilder
+ private func timerContent(_ timer: TimerSnapshot) -> some View {
+ VStack(alignment: .leading, spacing: 6) {
+ // Urgency indicator + label
+ HStack(spacing: 4) {
+ Circle()
+ .fill(urgencyColor(timer.urgency))
+ .frame(width: 8, height: 8)
+ Text(timer.label)
+ .font(.system(size: 13, weight: .semibold))
+ .foregroundStyle(.primary)
+ .lineLimit(1)
+ }
+
+ Spacer()
+
+ // Countdown using SwiftUI Date-relative text
+ if timer.state == .paused {
+ Text("Paused")
+ .font(.system(size: 28, weight: .bold, design: .monospaced))
+ .foregroundStyle(.secondary)
+ } else {
+ Text(timer.targetTime, style: .timer)
+ .font(.system(size: 28, weight: .bold, design: .monospaced))
+ .foregroundStyle(.primary)
+ .multilineTextAlignment(.leading)
+ }
+
+ // Target time
+ Text(formatTime(timer.targetTime))
+ .font(.system(size: 11, weight: .medium))
+ .foregroundStyle(.secondary)
+ }
+ .padding()
+ .containerBackground(for: .widget) {
+ urgencyColor(timer.urgency).opacity(0.1)
+ }
+ .widgetURL(URL(string: "chronomind://timer/\(timer.id)"))
+ }
+
+ private var emptyContent: some View {
+ VStack(spacing: 8) {
+ Image(systemName: "clock")
+ .font(.system(size: 28))
+ .foregroundStyle(.secondary)
+ Text("No Timers")
+ .font(.system(size: 13, weight: .medium))
+ .foregroundStyle(.secondary)
+ Text("Tap to create one")
+ .font(.system(size: 11))
+ .foregroundStyle(.tertiary)
+ }
+ .padding()
+ .containerBackground(for: .widget) {
+ Color(.systemBackground)
+ }
+ .widgetURL(URL(string: "chronomind://create"))
+ }
+
+ private func urgencyColor(_ urgency: UrgencyLevel) -> Color {
+ switch urgency {
+ case .critical: return Color(red: 1.0, green: 0.28, blue: 0.34)
+ case .important: return Color(red: 1.0, green: 0.62, blue: 0.26)
+ case .standard: return Color(red: 0.99, green: 0.79, blue: 0.34)
+ case .gentle: return Color(red: 0.18, green: 0.84, blue: 0.45)
+ case .passive: return Color(red: 0.36, green: 0.55, blue: 0.93)
+ }
+ }
+}
+
+// MARK: - Widget Definition
+
+struct NextTimerWidget: Widget {
+ let kind: String = "NextTimerWidget"
+
+ var body: some WidgetConfiguration {
+ AppIntentConfiguration(
+ kind: kind,
+ intent: NextTimerIntent.self,
+ provider: NextTimerProvider()
+ ) { entry in
+ NextTimerWidgetView(entry: entry)
+ }
+ .configurationDisplayName("Next Timer")
+ .description("Shows your next upcoming timer with countdown")
+ .supportedFamilies([.systemSmall])
+ }
+}
diff --git a/ios/ChronoMindWidgets/TimerListWidget.swift b/ios/ChronoMindWidgets/TimerListWidget.swift
new file mode 100644
index 0000000..2066570
--- /dev/null
+++ b/ios/ChronoMindWidgets/TimerListWidget.swift
@@ -0,0 +1,214 @@
+// ── Timer List Widget (Medium) ────────────────────────────────
+// Home screen medium widget showing next 3 timers as mini-timeline
+
+import SwiftUI
+import WidgetKit
+import AppIntents
+
+// MARK: - Timeline Provider
+
+struct TimerListProvider: AppIntentTimelineProvider {
+ typealias Entry = TimerListEntry
+ typealias Intent = TimerListIntent
+
+ func placeholder(in context: Context) -> TimerListEntry {
+ TimerListEntry(date: Date(), timers: Self.placeholderTimers)
+ }
+
+ func snapshot(for configuration: TimerListIntent, in context: Context) async -> TimerListEntry {
+ let timers = SharedTimerDataManager.shared.readActiveSnapshots()
+ return TimerListEntry(date: Date(), timers: Array(timers.prefix(3)))
+ }
+
+ func timeline(for configuration: TimerListIntent, in context: Context) async -> Timeline {
+ let timers = Array(SharedTimerDataManager.shared.readActiveSnapshots().prefix(3))
+ let now = Date()
+
+ var entries: [TimerListEntry] = []
+ for minuteOffset in stride(from: 0, through: 30, by: 5) {
+ let entryDate = Calendar.current.date(byAdding: .minute, value: minuteOffset, to: now)!
+ entries.append(TimerListEntry(date: entryDate, timers: timers))
+ }
+
+ let reloadDate: Date
+ if let firstTimer = timers.first {
+ reloadDate = min(firstTimer.targetTime, now.addingTimeInterval(30 * 60))
+ } else {
+ reloadDate = now.addingTimeInterval(30 * 60)
+ }
+
+ return Timeline(entries: entries, policy: .after(reloadDate))
+ }
+
+ static let placeholderTimers: [TimerSnapshot] = [
+ TimerSnapshot(
+ id: "p1", label: "Team Standup", type: .alarm, urgency: .important, state: .active,
+ targetTime: Date().addingTimeInterval(3600), duration: 3600, startedAt: Date(),
+ elapsedBeforePause: 0, snoozeCount: 0, category: nil,
+ pomodoroCurrentRound: nil, pomodoroTotalRounds: nil, pomodoroIsBreak: nil,
+ nextWarningTime: Date().addingTimeInterval(1800), totalWarnings: 3, firedWarnings: 1
+ ),
+ TimerSnapshot(
+ id: "p2", label: "Dentist", type: .alarm, urgency: .standard, state: .active,
+ targetTime: Date().addingTimeInterval(7200), duration: 7200, startedAt: Date(),
+ elapsedBeforePause: 0, snoozeCount: 0, category: nil,
+ pomodoroCurrentRound: nil, pomodoroTotalRounds: nil, pomodoroIsBreak: nil,
+ nextWarningTime: nil, totalWarnings: 2, firedWarnings: 0
+ ),
+ TimerSnapshot(
+ id: "p3", label: "Pick up laundry", type: .countdown, urgency: .gentle, state: .active,
+ targetTime: Date().addingTimeInterval(10800), duration: 10800, startedAt: Date(),
+ elapsedBeforePause: 0, snoozeCount: 0, category: nil,
+ pomodoroCurrentRound: nil, pomodoroTotalRounds: nil, pomodoroIsBreak: nil,
+ nextWarningTime: nil, totalWarnings: 1, firedWarnings: 0
+ ),
+ ]
+}
+
+// MARK: - Intent
+
+struct TimerListIntent: WidgetConfigurationIntent {
+ static var title: LocalizedStringResource = "Timer List"
+ static var description: IntentDescription = "Shows your next 3 upcoming timers"
+}
+
+// MARK: - Entry
+
+struct TimerListEntry: TimelineEntry {
+ let date: Date
+ let timers: [TimerSnapshot]
+}
+
+// MARK: - Widget View
+
+struct TimerListWidgetView: View {
+ let entry: TimerListEntry
+
+ var body: some View {
+ if entry.timers.isEmpty {
+ emptyContent
+ } else {
+ timerListContent
+ }
+ }
+
+ private var timerListContent: some View {
+ VStack(alignment: .leading, spacing: 0) {
+ // Header
+ HStack {
+ Image(systemName: "clock.fill")
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ Text("UPCOMING")
+ .font(.system(size: 10, weight: .semibold))
+ .foregroundStyle(.secondary)
+ .tracking(0.5)
+ Spacer()
+ Text("\(entry.timers.count) timer\(entry.timers.count == 1 ? "" : "s")")
+ .font(.system(size: 10, weight: .medium))
+ .foregroundStyle(.tertiary)
+ }
+ .padding(.bottom, 6)
+
+ // Timer rows
+ ForEach(Array(entry.timers.enumerated()), id: \.element.id) { index, timer in
+ if index > 0 {
+ Divider()
+ .padding(.vertical, 2)
+ }
+ timerRow(timer)
+ }
+
+ if entry.timers.count < 3 {
+ Spacer()
+ }
+ }
+ .padding()
+ .containerBackground(for: .widget) {
+ Color(.systemBackground)
+ }
+ }
+
+ private func timerRow(_ timer: TimerSnapshot) -> some View {
+ HStack(spacing: 8) {
+ // Urgency bar
+ RoundedRectangle(cornerRadius: 2)
+ .fill(urgencyColor(timer.urgency))
+ .frame(width: 3, height: 32)
+
+ VStack(alignment: .leading, spacing: 2) {
+ Text(timer.label)
+ .font(.system(size: 13, weight: .medium))
+ .foregroundStyle(.primary)
+ .lineLimit(1)
+ Text(formatTime(timer.targetTime))
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ }
+
+ Spacer()
+
+ // Countdown
+ if timer.state == .paused {
+ Text("Paused")
+ .font(.system(size: 12, weight: .medium, design: .monospaced))
+ .foregroundStyle(.secondary)
+ } else {
+ Text(timer.targetTime, style: .timer)
+ .font(.system(size: 14, weight: .semibold, design: .monospaced))
+ .foregroundStyle(urgencyColor(timer.urgency))
+ .multilineTextAlignment(.trailing)
+ .frame(minWidth: 50, alignment: .trailing)
+ }
+ }
+ .widgetURL(URL(string: "chronomind://timer/\(timer.id)"))
+ }
+
+ private var emptyContent: some View {
+ VStack(spacing: 8) {
+ Image(systemName: "clock")
+ .font(.system(size: 32))
+ .foregroundStyle(.secondary)
+ Text("No Active Timers")
+ .font(.system(size: 14, weight: .medium))
+ .foregroundStyle(.secondary)
+ Text("Tap to create one")
+ .font(.system(size: 12))
+ .foregroundStyle(.tertiary)
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .containerBackground(for: .widget) {
+ Color(.systemBackground)
+ }
+ .widgetURL(URL(string: "chronomind://create"))
+ }
+
+ private func urgencyColor(_ urgency: UrgencyLevel) -> Color {
+ switch urgency {
+ case .critical: return Color(red: 1.0, green: 0.28, blue: 0.34)
+ case .important: return Color(red: 1.0, green: 0.62, blue: 0.26)
+ case .standard: return Color(red: 0.99, green: 0.79, blue: 0.34)
+ case .gentle: return Color(red: 0.18, green: 0.84, blue: 0.45)
+ case .passive: return Color(red: 0.36, green: 0.55, blue: 0.93)
+ }
+ }
+}
+
+// MARK: - Widget Definition
+
+struct TimerListWidget: Widget {
+ let kind: String = "TimerListWidget"
+
+ var body: some WidgetConfiguration {
+ AppIntentConfiguration(
+ kind: kind,
+ intent: TimerListIntent.self,
+ provider: TimerListProvider()
+ ) { entry in
+ TimerListWidgetView(entry: entry)
+ }
+ .configurationDisplayName("Timer List")
+ .description("Shows your next 3 upcoming timers in a mini-timeline")
+ .supportedFamilies([.systemMedium])
+ }
+}
diff --git a/ios/ChronoMindWidgets/TimerLiveActivity.swift b/ios/ChronoMindWidgets/TimerLiveActivity.swift
new file mode 100644
index 0000000..68a9eb2
--- /dev/null
+++ b/ios/ChronoMindWidgets/TimerLiveActivity.swift
@@ -0,0 +1,215 @@
+// ── Timer Live Activity ───────────────────────────────────────
+// Dynamic Island + Lock Screen Live Activity for active timer countdown
+
+import SwiftUI
+import WidgetKit
+import ActivityKit
+
+// MARK: - Live Activity Widget
+
+struct TimerLiveActivity: Widget {
+ var body: some WidgetConfiguration {
+ ActivityConfiguration(for: TimerActivityAttributes.self) { context in
+ // Lock Screen Live Activity
+ lockScreenView(context: context)
+ } dynamicIsland: { context in
+ DynamicIsland {
+ // Expanded regions
+ DynamicIslandExpandedRegion(.leading) {
+ expandedLeading(context: context)
+ }
+ DynamicIslandExpandedRegion(.trailing) {
+ expandedTrailing(context: context)
+ }
+ DynamicIslandExpandedRegion(.center) {
+ expandedCenter(context: context)
+ }
+ DynamicIslandExpandedRegion(.bottom) {
+ expandedBottom(context: context)
+ }
+ } compactLeading: {
+ // Compact leading — urgency dot + label
+ HStack(spacing: 4) {
+ Circle()
+ .fill(colorFromHex(context.state.urgencyHex))
+ .frame(width: 8, height: 8)
+ Text(context.attributes.timerLabel)
+ .font(.system(size: 12, weight: .medium))
+ .lineLimit(1)
+ .frame(maxWidth: 60)
+ }
+ } compactTrailing: {
+ // Compact trailing — countdown
+ Text(context.state.targetTime, style: .timer)
+ .font(.system(size: 12, weight: .bold, design: .monospaced))
+ .foregroundStyle(colorFromHex(context.state.urgencyHex))
+ .frame(minWidth: 40)
+ .multilineTextAlignment(.trailing)
+ } minimal: {
+ // Minimal — just countdown
+ Text(context.state.targetTime, style: .timer)
+ .font(.system(size: 11, weight: .bold, design: .monospaced))
+ .multilineTextAlignment(.center)
+ }
+ }
+ }
+
+ // MARK: - Lock Screen View
+
+ @ViewBuilder
+ private func lockScreenView(context: ActivityViewContext) -> some View {
+ let urgencyColor = colorFromHex(context.state.urgencyHex)
+
+ VStack(spacing: 8) {
+ HStack {
+ // Urgency indicator + label
+ HStack(spacing: 6) {
+ Circle()
+ .fill(urgencyColor)
+ .frame(width: 10, height: 10)
+ Text(context.attributes.timerLabel)
+ .font(.system(size: 15, weight: .semibold))
+ .foregroundStyle(.white)
+ }
+
+ Spacer()
+
+ // Pomodoro round indicator
+ if let round = context.state.pomodoroRound,
+ let total = context.state.pomodoroTotal {
+ Text(context.state.isBreak ? "Break" : "Round \(round)/\(total)")
+ .font(.system(size: 12, weight: .medium))
+ .foregroundStyle(.white.opacity(0.7))
+ .padding(.horizontal, 8)
+ .padding(.vertical, 3)
+ .background(urgencyColor.opacity(0.3))
+ .clipShape(Capsule())
+ }
+ }
+
+ HStack(alignment: .bottom) {
+ // Large countdown
+ Text(context.state.targetTime, style: .timer)
+ .font(.system(size: 36, weight: .bold, design: .monospaced))
+ .foregroundStyle(.white)
+ .contentTransition(.numericText())
+
+ Spacer()
+
+ // Target time
+ VStack(alignment: .trailing, spacing: 2) {
+ Text("fires at")
+ .font(.system(size: 10))
+ .foregroundStyle(.white.opacity(0.5))
+ Text(context.state.targetTime, style: .time)
+ .font(.system(size: 14, weight: .medium))
+ .foregroundStyle(.white.opacity(0.8))
+ }
+ }
+
+ // Urgency progress bar
+ GeometryReader { geo in
+ ZStack(alignment: .leading) {
+ RoundedRectangle(cornerRadius: 2)
+ .fill(.white.opacity(0.15))
+ .frame(height: 3)
+ RoundedRectangle(cornerRadius: 2)
+ .fill(urgencyColor)
+ .frame(width: max(0, geo.size.width * progressFraction(context.state)), height: 3)
+ }
+ }
+ .frame(height: 3)
+ }
+ .padding(16)
+ .activityBackgroundTint(Color.black.opacity(0.85))
+ .activitySystemActionForegroundColor(.white)
+ }
+
+ // MARK: - Dynamic Island Expanded Views
+
+ @ViewBuilder
+ private func expandedLeading(context: ActivityViewContext) -> some View {
+ VStack(alignment: .leading, spacing: 2) {
+ Text(context.attributes.timerLabel)
+ .font(.system(size: 14, weight: .semibold))
+ .lineLimit(1)
+ if let round = context.state.pomodoroRound,
+ let total = context.state.pomodoroTotal {
+ Text(context.state.isBreak ? "Break" : "Round \(round)/\(total)")
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ }
+ }
+ }
+
+ @ViewBuilder
+ private func expandedTrailing(context: ActivityViewContext) -> some View {
+ Text(context.state.targetTime, style: .timer)
+ .font(.system(size: 20, weight: .bold, design: .monospaced))
+ .foregroundStyle(colorFromHex(context.state.urgencyHex))
+ .contentTransition(.numericText())
+ .multilineTextAlignment(.trailing)
+ }
+
+ @ViewBuilder
+ private func expandedCenter(context: ActivityViewContext) -> some View {
+ EmptyView()
+ }
+
+ @ViewBuilder
+ private func expandedBottom(context: ActivityViewContext) -> some View {
+ HStack(spacing: 16) {
+ // Fires at
+ HStack(spacing: 4) {
+ Image(systemName: "bell.fill")
+ .font(.system(size: 10))
+ .foregroundStyle(.secondary)
+ Text(context.state.targetTime, style: .time)
+ .font(.system(size: 12, weight: .medium))
+ .foregroundStyle(.secondary)
+ }
+
+ Spacer()
+
+ // Type indicator
+ HStack(spacing: 4) {
+ Image(systemName: typeIcon(context.attributes.timerType))
+ .font(.system(size: 10))
+ .foregroundStyle(.secondary)
+ Text(context.attributes.timerType.capitalized)
+ .font(.system(size: 12, weight: .medium))
+ .foregroundStyle(.secondary)
+ }
+ }
+ }
+
+ // MARK: - Helpers
+
+ private func progressFraction(_ state: TimerActivityAttributes.ContentState) -> CGFloat {
+ let remaining = state.targetTime.timeIntervalSinceNow
+ let total = max(1, TimeInterval(state.remainingSeconds))
+ let fraction = 1.0 - (remaining / total)
+ return CGFloat(min(1, max(0, fraction)))
+ }
+
+ private func colorFromHex(_ hex: String) -> Color {
+ switch hex {
+ case "#FF4757": return Color(red: 1.0, green: 0.28, blue: 0.34)
+ case "#FF9F43": return Color(red: 1.0, green: 0.62, blue: 0.26)
+ case "#FECA57": return Color(red: 0.99, green: 0.79, blue: 0.34)
+ case "#2ED573": return Color(red: 0.18, green: 0.84, blue: 0.45)
+ case "#A5B1C7": return Color(red: 0.65, green: 0.69, blue: 0.78)
+ default: return Color(red: 0.36, green: 0.55, blue: 0.93) // accent
+ }
+ }
+
+ private func typeIcon(_ type: String) -> String {
+ switch type {
+ case "alarm": return "alarm.fill"
+ case "countdown": return "timer"
+ case "pomodoro": return "target"
+ case "event": return "calendar"
+ default: return "clock"
+ }
+ }
+}