import SwiftUI import SwiftData #if canImport(UIKit) import UIKit #endif import Data import HabitTray import Interaction import Timeline public struct DayView: View { @Environment(HabitDragCoordinator.self) private var dragCoordinator @Environment(\.modelContext) private var modelContext @State private var selectedDate: Date = Calendar.current.startOfDay(for: .now) @State private var hoveredSlot: Int? @State private var timelineFrame: CGRect = .zero @State private var currentScrollOffset: CGFloat = 0 @State private var lastHapticSlot: Int? private let referenceDate: Date = Calendar.current.startOfDay(for: .now) private let pageRange: Int = 365 private let initialScrollSlot: Int = min(TimelineLayout.totalSlots - 1, TimelineLayout.slotIndex(for: .now) + 3) public init() {} public var body: some View { VStack(spacing: 0) { DateHeaderView(selectedDate: $selectedDate) TabView(selection: pageIndexBinding) { ForEach(-pageRange...pageRange, id: \.self) { offset in DayTimelineView( date: date(for: offset), initialScrollSlot: initialScrollSlot, hoveredSlot: $hoveredSlot, scrollOffset: $currentScrollOffset ) .tag(offset) } } .tabViewStyle(.page(indexDisplayMode: .never)) .onGeometryChange(for: CGRect.self) { proxy in proxy.frame(in: .global) } action: { frame in timelineFrame = frame } } .onChange(of: dragCoordinator.dragLocation) { _, location in guard dragCoordinator.isActive else { return } hoveredSlot = slot(for: location) } .onChange(of: dragCoordinator.pendingDrop) { _, pendingDrop in guard pendingDrop else { return } handleDrop() } .onChange(of: hoveredSlot) { _, newSlot in guard dragCoordinator.isActive, let slot = newSlot, slot != lastHapticSlot else { return } hapticSnap() lastHapticSlot = slot } .sheet(isPresented: .constant(true)) { HabitTrayView() } } private var pageIndexBinding: Binding { Binding( get: { Calendar.current.dateComponents([.day], from: referenceDate, to: selectedDate).day ?? 0 }, set: { offset in selectedDate = date(for: offset) } ) } private func date(for offset: Int) -> Date { Calendar.current.date(byAdding: .day, value: offset, to: referenceDate) ?? referenceDate } private func slot(for point: CGPoint) -> Int? { guard timelineFrame.contains(point) else { return nil } let localY = point.y - timelineFrame.minY let contentY = localY + currentScrollOffset let slot = Int(contentY / TimelineLayout.slotHeight) guard (0..