# Post-it note for SwiftUI — agent installation guide

This document contains the complete distributable source, usage example,
installation requirements, and MIT license from Daniel Paul's UI Kit.
The HTML demo and ZIP download are generated from the same files.

## Install in an existing project

1. Target platform: SwiftUI. Read the project's instructions and inspect its
   framework, supported OS/runtime, package manager, and component conventions.
   Reuse the existing app and compatible dependencies. Do not replace its framework.
2. Requirements: An iOS 18+ SwiftUI app, built with Xcode 16 or newer. Uses SwiftUI, UIKit, and Foundation, with no third-party packages or font files.
   No third-party dependency installation is required; see the README for framework requirements.
3. Copy every component file and the MIT license below together. A suggested
   location is `src/components/post-it-note/`; adapt to the project's conventions
   and preserve relative imports. The platform-specific README and usage example explain
   integration, resources, and accessibility.
4. Integrate the component where the user asks. If no location is specified,
   add a small example in an appropriate existing preview or screen and report
   where to find it. Connect real app state and persistence only where needed;
   do not claim that local preview edits have been saved remotely.
5. Preserve keyboard access, visible focus, reduced-motion behavior, and the
   license. Run the relevant type checks and build, and report any checks that
   could not run. Do not deploy or publish without the user's authorization.

## Complete files

## PostItNote.swift

```swift
// SPDX-License-Identifier: MIT — Copyright (c) 2026 Daniel Paul
import SwiftUI
import UIKit

@available(iOS 18.0, *)
struct PostItNote: View {
    enum Paper: CaseIterable {
        case yellow, rose, blue, lilac
        var channels: (Double, Double, Double) {
            switch self {
            case .yellow: (250, 231, 160)
            case .rose: (246, 219, 226)
            case .blue: (223, 234, 248)
            case .lilac: (233, 223, 245)
            }
        }
        var color: Color { Color(red: channels.0 / 255, green: channels.1 / 255, blue: channels.2 / 255) }
    }

    @Binding var text: String
    var title = ""
    var dateLabel = ""
    var color: Paper = .yellow
    var width: CGFloat = 292
    var rotation: Double = 0
    var editable = false
    var breeze = true
    var texture = true
    var maxLength = 500
    @Environment(\.accessibilityReduceMotion) private var reducedMotion
    @Environment(\.scenePhase) private var scenePhase
    @State private var editing = false
    @State private var moreAbove = false
    @State private var moreBelow = false
    @State private var startedAt = Date()
    @State private var pausedAt: Date?
    private var running: Bool { breeze && !reducedMotion && !editing && scenePhase == .active }
    private let ink = Color(red: 36/255, green: 78/255, blue: 146/255)

    var body: some View {
        ZStack {
            TimelineView(.animation(minimumInterval: 1/30, paused: !running)) { timeline in
                Canvas { context, size in
                    let phase = (pausedAt ?? timeline.date).timeIntervalSince(startedAt) / 6.5
                    let swell = (1 - cos(phase * .pi * 2)) / 2
                    let amount = reducedMotion || !breeze ? 0.3 : 0.12 + 0.8 * swell
                    let flutter = reducedMotion || !breeze ? 0 : sin(phase * .pi * 6) * pow(swell, 1.5)
                    let sheet = paperPaths(size: size, amount: amount, flutter: flutter)
                    context.fill(sheet.front, with: .color(color.color))
                    for strip in sheet.strips {
                        let tone = paperTone(angle: strip.angle, back: strip.back)
                        context.fill(strip.path, with: .color(tone))
                        context.stroke(strip.path, with: .color(tone), lineWidth: 0.5)
                    }
                    context.stroke(sheet.outline, with: .color(.brown.opacity(0.18)), lineWidth: 0.7)
                    context.clip(to: sheet.outline)
                    if texture {
                        for index in 0..<1500 {
                            let x = CGFloat((index * 193 + 17) % 997) / 997 * size.width
                            let y = CGFloat((index * 347 + 39) % 991) / 991 * size.height
                            let dot = CGRect(x: x, y: y, width: 0.65, height: 0.8)
                            context.fill(Path(ellipseIn: dot), with: .color(.brown.opacity(0.09)))
                        }
                    }

                }
            }
            .allowsHitTesting(false)
            VStack(spacing: 12) {
                if !title.isEmpty {
                    Text(title).font(.system(size: 12, weight: .semibold)).accessibilityAddTraits(.isHeader)
                }
                NativeWriting(text: $text, editable: editable, maxLength: maxLength, title: title, editing: $editing, moreAbove: $moreAbove, moreBelow: $moreBelow)
                    .background(editing ? ink.opacity(0.05) : .clear, in: RoundedRectangle(cornerRadius: 5))
                    .mask {
                        GeometryReader { geometry in
                            let fade = min(0.35, 44 / max(1, geometry.size.height))
                            LinearGradient(stops: [
                                .init(color: moreAbove ? .clear : .black, location: 0),
                                .init(color: .black, location: fade),
                                .init(color: .black, location: 1 - fade),
                                .init(color: moreBelow ? .clear : .black, location: 1)
                            ], startPoint: .top, endPoint: .bottom)
                        }
                    }
                if !dateLabel.isEmpty { Text(dateLabel).font(.system(size: 11)).foregroundStyle(.brown) }
            }
            .foregroundStyle(Color(red: 69/255, green: 59/255, blue: 29/255))
            .padding(.horizontal, width * 0.09)
            .padding(.top, width * 0.08)
            .padding(.bottom, width * 0.1)
        }
        .frame(width: width, height: width)
        .rotationEffect(.degrees(rotation))
        .shadow(color: .black.opacity(0.13), radius: 12, x: 0, y: 10)
        .onChange(of: running) { _, isRunning in
            if isRunning {
                if let pause = pausedAt { startedAt = startedAt.addingTimeInterval(Date().timeIntervalSince(pause)) }
                pausedAt = nil
            } else { pausedAt = Date() }
        }
        .onAppear { if !running { pausedAt = Date() } }
    }

    private func paperTone(angle: Double, back: Bool) -> Color {
        let white = back ? 0.32 : max(0, 0.05 * cos(angle - 0.35))
        let shade = (1 - abs(cos(angle - 0.4))) * 0.12
        func channel(_ base: Double, _ shadow: Double) -> Double {
            let lit = base + (255 - base) * white
            return (lit + (shadow - lit) * shade) / 255
        }
        return Color(red: channel(color.channels.0, 66), green: channel(color.channels.1, 48), blue: channel(color.channels.2, 25))
    }
    private func paperPaths(size: CGSize, amount: Double, flutter: Double) -> (front: Path, outline: Path, strips: [(path: Path, angle: Double, back: Bool)]) {
        let w = size.width - 1, h = size.height - 1
        let length = min(size.width, size.height) * 0.25
        let reach = length / sqrt(2.0)
        let turn = 0.65 + amount * 1.85 + flutter * 0.07
        let radius = reach / turn
        func project(_ t: Double, right: Bool) -> CGPoint {
            let distance = reach * t, angle = distance / radius
            let travel = radius * sin(angle), z = radius * (1 - cos(angle))
            let inset = (travel - distance) / sqrt(2.0)
            return CGPoint(x: (right ? w : w - length + length * t) + inset - z * 0.1,
                           y: (right ? h - length + length * t : h) + inset - z * 0.6)
        }
        let right = (0...24).map { project(Double($0) / 24, right: true) }
        let bottom = (0...24).map { project(Double($0) / 24, right: false) }
        func start(_ path: inout Path) {
            path.move(to: CGPoint(x: 4, y: 0.5))
            path.addLine(to: CGPoint(x: w - 3, y: 0.5))
            path.addQuadCurve(to: CGPoint(x: w, y: 4), control: CGPoint(x: w, y: 0.5))
        }
        func finish(_ path: inout Path) {
            path.addLine(to: CGPoint(x: 4, y: h))
            path.addQuadCurve(to: CGPoint(x: 0.5, y: h - 3), control: CGPoint(x: 0.5, y: h))
            path.addLine(to: CGPoint(x: 0.5, y: 4))
            path.addQuadCurve(to: CGPoint(x: 4, y: 0.5), control: CGPoint(x: 0.5, y: 0.5))
            path.closeSubpath()
        }
        var front = Path()
        start(&front)
        front.addLine(to: right[0])
        front.addLine(to: bottom[0])
        finish(&front)
        var outline = Path()
        start(&outline)
        for point in right + bottom.reversed() { outline.addLine(to: point) }
        finish(&outline)
        let strips = (1...24).map { i -> (path: Path, angle: Double, back: Bool) in
            var path = Path()
            path.move(to: right[i - 1]); path.addLine(to: right[i])
            path.addLine(to: bottom[i]); path.addLine(to: bottom[i - 1]); path.closeSubpath()
            let angle = turn * (Double(i) - 0.5) / 24
            let back = cos(angle) - 0.7 / sqrt(2.0) * sin(angle) < 0
            return (path, angle, back)
        }
        return (front, outline, strips)
    }
}

@available(iOS 18.0, *)
private struct NativeWriting: UIViewRepresentable {
    @Binding var text: String
    let editable: Bool
    let maxLength: Int
    let title: String
    @Binding var editing: Bool
    @Binding var moreAbove: Bool
    @Binding var moreBelow: Bool

    func makeCoordinator() -> Coordinator { Coordinator(self) }
    func makeUIView(context: Context) -> UITextView {
        let view = UITextView()
        view.delegate = context.coordinator
        view.backgroundColor = .clear
        view.font = UIFont(name: "ChalkboardSE-Regular", size: 24) ?? .systemFont(ofSize: 24)
        view.textColor = UIColor(red: 36/255, green: 78/255, blue: 146/255, alpha: 1)
        view.tintColor = view.textColor
        view.textAlignment = .center
        view.textContainerInset = UIEdgeInsets(top: 12, left: 6, bottom: 12, right: 6)
        view.showsVerticalScrollIndicator = false
        view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
        return view
    }
    func updateUIView(_ view: UITextView, context: Context) {
        context.coordinator.parent = self
        if view.text != text { view.text = text }
        view.isEditable = editable
        view.accessibilityLabel = title.isEmpty ? "Note text" : "\(title) — note text"
        DispatchQueue.main.async { [weak view, weak coordinator = context.coordinator] in
            if let view { coordinator?.scrollViewDidScroll(view) }
        }
    }
    final class Coordinator: NSObject, UITextViewDelegate {
        var parent: NativeWriting
        init(_ parent: NativeWriting) { self.parent = parent }
        func textViewDidBeginEditing(_ textView: UITextView) { parent.editing = true }
        func textViewDidEndEditing(_ textView: UITextView) { parent.editing = false }
        func textViewDidChange(_ textView: UITextView) { parent.text = textView.text; scrollViewDidScroll(textView) }
        func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
            guard let swiftRange = Range(range, in: textView.text) else { return false }
            return textView.text.replacingCharacters(in: swiftRange, with: text).count <= parent.maxLength
        }
        func scrollViewDidScroll(_ scrollView: UIScrollView) {
            let up = scrollView.contentOffset.y > 2
            let down = scrollView.contentSize.height - scrollView.bounds.height - scrollView.contentOffset.y > 2
            if parent.moreAbove != up { parent.moreAbove = up }
            if parent.moreBelow != down { parent.moreBelow = down }
        }
    }
}
```

## Usage.swift

```swift
import SwiftUI

@available(iOS 18.0, *)
struct NoteExample: View {
    @State private var note = "Make room for the small ideas.\n\nThey might turn into something."
    var body: some View {
        PostItNote(text: $note, title: "Note to self", dateLabel: "1 Nov 2026", color: .yellow, width: 280, rotation: -2, editable: true)
            .padding(24)
    }
}
```

## README.md

````text
# Post-it note for SwiftUI

An original iOS native implementation by Daniel Paul. Free under MIT.
Requires an iOS 18+ SwiftUI app and Xcode 16 or newer. Uses SwiftUI, UIKit, and
Foundation only. No package installation, custom font assets, or API keys.

## Add to an existing project

1. Copy PostItNote.swift and Usage.swift into your Xcode project, enable the
   app target's membership for both files, and retain LICENSE.txt.
2. Keep your app's deployment target at iOS 18 or higher. The example and
   component are availability annotated so an older app can provide a fallback.
3. Render NoteExample() from an existing screen. Do not add another @main app.
4. For your own screen, pass a Binding<String> into PostItNote(text:). The
   example's @State demonstrates local editing. Save from your parent model
   if persistence is required; the component performs no network/storage work.

## Usage

```swift
PostItNote(text: $note, title: "Note to self", dateLabel: "1 Nov 2026",
           color: .yellow, width: 280, rotation: -2, editable: true)
```

## API and behavior

- text: required Binding<String>.
- title / dateLabel: optional strings; remain fixed while writing scrolls.
- color: .yellow (default), .rose, .blue, or .lilac.
- width: square size in points; default 292. Choose a width that fits your layout.
- rotation: static angle in degrees, default 0; negative tilts counterclockwise.
  For example rotation: -2. The tilt remains with Reduce Motion.
- editable: false by default. Uses a selectable UITextView; true enables editing.
- breeze / texture: true by default.
- maxLength: 500 by default; limits user input, not values set by your model.

The blue handwriting uses the built-in iOS Chalkboard SE font, with a system
fallback. Canvas renders the single paper sheet, right-corner curl, and static
grain. The corner has a fixed footprint and material length; only its
cylindrical bend changes. This is a native equivalent of the design, not the browser's WebGL
shader. The left edge stays still. The corner pauses while editing, in the
background, and with Reduce Motion. Longer content scrolls inside; edge fades
show more writing above/below, without arrows or an editing border.

The UIKit bridge handles text editing, scroll measurement, and selection;
SwiftUI owns the square layout and fade mask. No third-party libraries.
Validate VoiceOver, text selection, dynamic type/accessibility requirements,
and animation performance in your target app. The website preview uses React;
run NoteExample in an iOS simulator/device to preview this implementation.
Keep the MIT license and source copyright notice when redistributing.
````

## LICENSE.txt

```text
MIT License

Copyright (c) 2026 Daniel Paul

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
