# Post-it note for React Native — 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: React Native. 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 existing Expo React Native app with TypeScript. Keep the React and React Native versions supplied by your Expo SDK. For bare React Native, install Expo modules first or adapt the font/gradient imports to your existing native libraries.
   Dependency command: `npx expo install react-native-svg expo-linear-gradient expo-font @expo-google-fonts/caveat`. Adapt it to the project's package manager and reuse compatible existing dependencies.
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.tsx

```tsx
// SPDX-License-Identifier: MIT — Copyright (c) 2026 Daniel Paul
import { useEffect, useId, useMemo, useRef, useState, type ComponentRef, type ReactElement } from 'react'
import { AccessibilityInfo, Animated, AppState, Easing, ScrollView, StyleSheet, Text, TextInput, View, type NativeScrollEvent, type NativeSyntheticEvent } from 'react-native'
import Svg, { Defs, Path, Pattern, Circle } from 'react-native-svg'
import { LinearGradient } from 'expo-linear-gradient'
import { paperGeometry, paperStripColor } from './paper-geometry'

export type NoteColor = 'yellow' | 'rose' | 'blue' | 'lilac'
export interface PostItNoteProps {
  title?: string
  value: string
  onValueChange?: (text: string) => void
  color?: NoteColor
  width?: number
  rotation?: number
  editable?: boolean
  breeze?: boolean
  texture?: boolean
  dateLabel?: string
  maxLength?: number
  fontFamily?: string
}
const papers = { yellow: '#fae7a0', rose: '#f6dbe2', blue: '#dfeaf8', lilac: '#e9dff5' }
const AnimatedPath = Animated.createAnimatedComponent(Path)

function paperPaths(width: number, phase: number, paper: string) {
  const swell = (1 - Math.cos(phase * Math.PI * 2)) / 2
  const amount = 0.12 + 0.8 * swell
  const flutter = Math.sin(phase * Math.PI * 6) * Math.pow(swell, 1.5)
  const { w, h, right, bottom, strips } = paperGeometry(width, width, amount, flutter)
  const points = (items: { x: number; y: number }[]) => items.map((p) => `${p.x} ${p.y}`).join('L')
  const start = `M4 .5H${w - 3}Q${w} .5 ${w} 4L`
  const end = `H4Q.5 ${h} .5 ${h - 3}V4Q.5 .5 4 .5Z`
  return {
    front: `${start}${points([right[0], bottom[0]])}${end}`,
    outline: `${start}${points([...right, ...bottom.slice().reverse()])}${end}`,
    strips: strips.map((strip) => ({ d: `M${points(strip.points)}Z`, color: paperStripColor(paper, strip.angle, strip.back) })),
  }
}

/** Expo native version. Load Caveat once in your app; see Usage.tsx. */
export function PostItNote({ title, value, onValueChange, color = 'yellow', width = 292, rotation = 0, editable = false, breeze = true, texture = true, dateLabel, maxLength = 500, fontFamily = 'Caveat_500Medium' }: PostItNoteProps): ReactElement {
  const progress = useRef(new Animated.Value(.2)).current
  const field = useRef<ComponentRef<typeof TextInput>>(null)
  const [focused, setFocused] = useState(false)
  const [reduced, setReduced] = useState(true)
  const [active, setActive] = useState(AppState.currentState === 'active')
  const [viewportHeight, setViewportHeight] = useState(0)
  const [contentHeight, setContentHeight] = useState(0)
  const [offset, setOffset] = useState(0)
  const id = useId().replace(/[^a-z0-9]/gi, '')
  const paper = papers[color]
  const padding = width * .09
  const paths = useMemo(() => {
    const inputRange = Array.from({ length: 65 }, (_, i) => i / 64)
    const samples = inputRange.map((phase) => paperPaths(width, phase, paper))
    return {
      front: progress.interpolate({ inputRange, outputRange: samples.map((sample) => sample.front) }),
      outline: progress.interpolate({ inputRange, outputRange: samples.map((sample) => sample.outline) }),
      strips: samples[0].strips.map((_, i) => ({
        d: progress.interpolate({ inputRange, outputRange: samples.map((sample) => sample.strips[i].d) }),
        color: progress.interpolate({ inputRange, outputRange: samples.map((sample) => sample.strips[i].color) }),
      })),
    }
  }, [paper, progress, width])

  useEffect(() => {
    let mounted = true
    void AccessibilityInfo.isReduceMotionEnabled().then((value) => { if (mounted) setReduced(value) })
    const motion = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduced)
    const app = AppState.addEventListener('change', (value) => setActive(value === 'active'))
    return () => { mounted = false; motion.remove(); app.remove() }
  }, [])
  useEffect(() => {
    if (!breeze || reduced) { progress.setValue(.2); return }
    if (!active || focused) return
    const animation = Animated.loop(Animated.sequence([
      Animated.timing(progress, { toValue: 1, duration: 3250, easing: Easing.linear, useNativeDriver: false, isInteraction: false }),
      Animated.timing(progress, { toValue: 0, duration: 3250, easing: Easing.linear, useNativeDriver: false, isInteraction: false }),
    ]))
    animation.start()
    return () => animation.stop()
  }, [active, breeze, focused, progress, reduced])
  const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => setOffset(event.nativeEvent.contentOffset.y)
  const moreAbove = offset > 2
  const moreBelow = contentHeight - viewportHeight - offset > 2
  const writing = { fontFamily, color: '#244e92', fontSize: 24, lineHeight: 29, textAlign: 'center' as const }

  return <View style={[styles.note, { width, height: width, transform: [{ rotate: `${rotation}deg` }] }]}>
    <Svg width={width} height={width} style={StyleSheet.absoluteFill} pointerEvents="none">
      <Defs><Pattern id={`grain${id}`} width="40" height="40" patternUnits="userSpaceOnUse">{Array.from({ length: 45 }, (_, i) => <Circle key={i} cx={(i * 17.31) % 40} cy={(i * 23.17) % 40} r="0.42" fill="#756335" opacity="0.13" />)}</Pattern></Defs>
      <AnimatedPath d={paths.front} fill={paper} />
      {paths.strips.map((strip, i) => <AnimatedPath key={i} d={strip.d} fill={strip.color} stroke={strip.color} strokeWidth="0.5" />)}
      {texture && <AnimatedPath d={paths.outline} fill={`url(#grain${id})`} />}
      <AnimatedPath d={paths.outline} fill="none" stroke="#756335" strokeOpacity="0.18" strokeWidth="0.7" />
    </Svg>
    <View style={[styles.content, { paddingHorizontal: padding, paddingTop: width * .08, paddingBottom: width * .1 }]}>
      {title && <Text accessibilityRole="header" style={styles.title} onPress={() => editable && field.current?.focus()}>{title}</Text>}
      <View style={styles.writing} onLayout={(event) => setViewportHeight(event.nativeEvent.layout.height)}>

          {editable ? <TextInput ref={field} accessibilityLabel={title ? `${title} — note text` : 'Note text'} value={value} onChangeText={onValueChange} multiline scrollEnabled maxLength={maxLength} onFocus={() => setFocused(true)} onBlur={() => setFocused(false)} onContentSizeChange={(event) => setContentHeight(event.nativeEvent.contentSize.height)} onScroll={onScroll} textAlignVertical="top" underlineColorAndroid="transparent" style={[styles.input, writing, focused && styles.highlight]} /> : <ScrollView onScroll={onScroll} scrollEventThrottle={32} onContentSizeChange={(_, height) => setContentHeight(height)} showsVerticalScrollIndicator={false} nestedScrollEnabled contentContainerStyle={styles.readOnlyContent}><Text selectable style={writing}>{value}</Text></ScrollView>}
        {moreAbove && <LinearGradient pointerEvents="none" colors={[paper, `${paper}00`]} style={styles.fadeTop} />}
        {moreBelow && <LinearGradient pointerEvents="none" colors={[`${paper}00`, paper]} style={styles.fadeBottom} />}
      </View>
      {dateLabel && <Text style={styles.date}>{dateLabel}</Text>}
    </View>
  </View>
}
const styles = StyleSheet.create({
  note: { shadowColor: '#453b1d', shadowOffset: { width: 0, height: 10 }, shadowOpacity: .16, shadowRadius: 12 },
  content: { flex: 1, gap: 12 },
  title: { fontSize: 12, lineHeight: 17, fontWeight: '600', color: '#453b1d', textAlign: 'center' },
  writing: { flex: 1, minHeight: 0 },
  fadeTop: { position: 'absolute', top: 0, left: 0, right: 0, height: 44 },
  fadeBottom: { position: 'absolute', bottom: 0, left: 0, right: 0, height: 44 },
  input: { flex: 1, paddingVertical: 12, paddingHorizontal: 6, borderRadius: 5 },
  highlight: { backgroundColor: 'rgba(36,78,146,0.05)' },
  readOnlyContent: { flexGrow: 1, justifyContent: 'center', paddingVertical: 8 },
  date: { color: '#756335', fontSize: 11, lineHeight: 15, textAlign: 'center' },
})
```

## paper-geometry.ts

```ts
// SPDX-License-Identifier: MIT — Copyright (c) 2026 Daniel Paul
export interface PaperPoint { x: number; y: number }
export interface PaperStrip { points: PaperPoint[]; angle: number; back: boolean }

/** Bend a fixed triangular corner around a cylinder without stretching it. */
export function paperGeometry(width: number, height: number, amount: number, flutter = 0) {
  const w = width - 1, h = height - 1
  const length = Math.min(width, height) * 0.25
  const reach = length / Math.SQRT2
  const turn = 0.65 + amount * 1.85 + flutter * 0.07
  const radius = reach / turn
  const cameraX = 0.1, cameraY = 0.6
  function project(t: number, side: 'right' | 'bottom'): PaperPoint {
    const distance = reach * t
    const angle = distance / radius
    const travel = radius * Math.sin(angle)
    const z = radius * (1 - Math.cos(angle))
    const inset = (travel - distance) / Math.SQRT2
    return {
      x: (side === 'right' ? w : w - length + length * t) + inset - z * cameraX,
      y: (side === 'bottom' ? h : h - length + length * t) + inset - z * cameraY,
    }
  }
  const right: PaperPoint[] = [], bottom: PaperPoint[] = [], strips: PaperStrip[] = []
  const steps = 24
  for (let i = 0; i <= steps; i++) {
    right.push(project(i / steps, 'right'))
    bottom.push(project(i / steps, 'bottom'))
    if (i) {
      const angle = turn * (i - 0.5) / steps
      strips.push({
        points: [right[i - 1], right[i], bottom[i], bottom[i - 1]], angle,
        back: Math.cos(angle) - (cameraX + cameraY) / Math.SQRT2 * Math.sin(angle) < 0,
      })
    }
  }
  return { w, h, length, right, bottom, strips }
}

/** Diffuse light on the same paper pigment, including the paler reverse face. */
export function paperStripColor(paper: string, angle: number, back: boolean) {
  if (!/^#[0-9a-f]{6}$/i.test(paper)) return paper
  const white = back ? 0.32 : Math.max(0, 0.05 * Math.cos(angle - 0.35))
  const shade = (1 - Math.abs(Math.cos(angle - 0.4))) * 0.12
  const shadow = [66, 48, 25]
  const channels = [1, 3, 5].map((offset, i) => {
    const base = parseInt(paper.slice(offset, offset + 2), 16)
    const lit = base + (255 - base) * white
    return Math.round(lit + (shadow[i] - lit) * shade)
  })
  return `rgb(${channels.join(',')})`
}
```

## Usage.tsx

```tsx
import { useState } from 'react'
import { ActivityIndicator, View, useWindowDimensions } from 'react-native'
import { useFonts, Caveat_500Medium } from '@expo-google-fonts/caveat'
import { PostItNote } from './PostItNote'

export default function Example() {
  const { width } = useWindowDimensions()
  const [loaded, error] = useFonts({ Caveat_500Medium })
  const [note, setNote] = useState('Make room for the small ideas.\n\nThey might turn into something.')
  if (error) throw error
  if (!loaded) return <ActivityIndicator accessibilityLabel="Loading handwriting font" />
  return <View style={{ padding: 24, alignItems: 'center' }}>
    <PostItNote title="Note to self" value={note} onValueChange={setNote} width={Math.min(292, width - 48)} rotation={-2} editable dateLabel="1 Nov 2026" />
  </View>
}
```

## README.md

````text
# Post-it note for React Native

An original native implementation by Daniel Paul. MIT. Built for an existing
Expo app with TypeScript, using your Expo SDK's matching React Native version.

## Install

```sh
npx expo install react-native-svg expo-linear-gradient expo-font @expo-google-fonts/caveat
```

Expo resolves versions compatible with your SDK. Do not replace the app's
React or React Native version. For bare React Native, configure Expo modules
first (https://docs.expo.dev/bare/installing-expo-modules/) and rebuild the
native app after installing the native dependencies. Follow your existing
package manager and CocoaPods workflow. No web CSS or Paper Design shader is
required; this version uses SVG grain and an animated native SVG outline.

Copy PostItNote.tsx, paper-geometry.ts, Usage.tsx, and LICENSE.txt together into your component
folder. Render Example from Usage.tsx on an existing screen. The example loads
Caveat_500Medium from the font package before displaying the note; move that
font loading to your app root when using multiple notes. The font is bundled
by Metro. No runtime request to Google Fonts is made.

## API

- value: required string; supply onValueChange when editable.
- title and dateLabel: optional fixed text above and below the writing.
- color: yellow, rose, blue, or lilac; default yellow.
- width: square size in logical pixels; default 292. Choose a size that fits
  the parent (for example Math.min(292, availableWidth)).
- rotation: static angle in degrees, default 0; negative tilts counterclockwise.
  For example rotation={-2}. The tilt remains with reduced motion.
- editable: false by default. Uses a scrollable native multiline TextInput.
- breeze and texture: true by default. Right edge only; the left stays still.
- maxLength: 500 by default.
- fontFamily: Caveat_500Medium by default; override if your app has a font.

Long notes scroll inside the paper, with fades when content is hidden above
or below. No arrow buttons. The note stays square and title/date stay fixed.
Editing, reduced motion, and backgrounded apps pause the corner. In a long
virtualized list, also set breeze=false for rows your app marks off screen.
The animation uses the JS driver for the SVG path. Keep large lists virtualized.
Changes are local until your parent saves value through onValueChange.

This source targets iOS/Android through Expo, not React Native Web. A fixed triangular corner bends around a cylinder without changing its material
length. paper-geometry.ts is shared with the React version. Native
SVG grain is an approximation of the website's WebGL paper shader. Validate
keyboard, font loading, scroll behavior, and performance on your target devices.

Keep LICENSE.txt. Dependencies retain their respective licenses. Caveat is
SIL Open Font License 1.1; preserve the font package's bundled license when
redistributing. Docs: https://docs.expo.dev/versions/latest/sdk/svg/
````

## 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.
```
