"use client"; import { ChevronDownIcon, ChevronLeftIcon, FolderIcon, Heading1Icon, Heading2Icon, ListChecksIcon, ListIcon, NotebookPenIcon, PilcrowIcon, PinIcon, RotateCcwIcon, SearchIcon, SquarePenIcon, Trash2Icon, Undo2Icon, XIcon, } from "lucide-react"; import { AnimatePresence, LayoutGroup, motion, useReducedMotion } from "motion/react"; import { type KeyboardEvent, type ReactNode, type TextareaHTMLAttributes, useEffect, useLayoutEffect, useRef, useState, } from "react"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group"; import { Menu, MenuGroup, MenuGroupLabel, MenuItem, MenuPopup, MenuRadioGroup, MenuRadioItem, MenuTrigger, } from "@/components/ui/menu"; function useReducedMotionSafe() { const prefers = useReducedMotion(); const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []); return mounted && Boolean(prefers); } export type NoteBlockType = "h1" | "h2" | "p" | "todo" | "bullet"; export interface NoteBlock { id: string; type: NoteBlockType; text: string; done?: boolean; } export type NoteFolderTone = "amber" | "sky" | "rose" | "emerald" | "violet"; export interface NoteFolder { id: string; name: string; tone: NoteFolderTone; } export interface Note { id: string; title: string; folderId: string; updatedAt: string; pinned?: boolean; deleted?: boolean; blocks: NoteBlock[]; } export interface NotesProps { className?: string; folders?: NoteFolder[]; notes?: Note[]; now?: string; initialNoteId?: string; } const easeOut = [0.23, 1, 0.32, 1] as const; const spring = { type: "spring", stiffness: 520, damping: 46, mass: 1 } as const; const press = "transition-[color,background-color,box-shadow,scale] duration-150 ease-out active:scale-[0.97]"; const ring = "outline-none focus-visible:ring-2 focus-visible:ring-ring"; const menuPop = "transition-[opacity,scale] duration-150 ease-[cubic-bezier(0.23,1,0.32,1)] data-starting-style:scale-95 data-starting-style:opacity-0 data-ending-style:scale-95 data-ending-style:opacity-0 data-ending-style:duration-100"; const tones: Record = { amber: "bg-amber-500 dark:bg-amber-400", sky: "bg-sky-500 dark:bg-sky-400", rose: "bg-rose-500 dark:bg-rose-400", emerald: "bg-emerald-500 dark:bg-emerald-400", violet: "bg-violet-500 dark:bg-violet-400", }; const defaultFolders: NoteFolder[] = [ { id: "personal", name: "Personal", tone: "amber" }, { id: "work", name: "Work", tone: "sky" }, { id: "recipes", name: "Recipes", tone: "rose" }, { id: "travel", name: "Travel", tone: "emerald" }, ]; let seed = 0; const b = (type: NoteBlockType, text: string, done?: boolean): NoteBlock => ({ id: `b${++seed}`, type, text, done }); const defaultNotes: Note[] = [ { id: "n1", title: "Lisbon in May", folderId: "travel", updatedAt: "2026-10-24T09:41:00", pinned: true, blocks: [ b("p", "Five nights, flying in on the 14th. Staying in Príncipe Real, ten minutes up the hill from Bairro Alto."), b("h2", "Before we go"), b("todo", "Renew passport", true), b("todo", "Book the 28 tram early, before the queues", true), b("todo", "Pack a travel adapter (type F)"), b("todo", "Download offline maps for Sintra"), b("h2", "Eat"), b("bullet", "Custard tarts warm from the oven, first morning"), b("bullet", "Grilled sardines somewhere in Alfama"), b("bullet", "Ask Rui for the tasca with no sign on the door"), b("h2", "Maybe"), b("p", "Day trip to Cascais if the weather holds. Train from Cais do Sodré, 40 minutes."), ], }, { id: "n2", title: "Q4 planning", folderId: "work", updatedAt: "2026-10-23T16:20:00", pinned: true, blocks: [ b("p", "Three bets for the quarter. Everything else waits for January."), b("h2", "Bets"), b("bullet", "Self-serve onboarding: cut time to first project under 5 minutes"), b("bullet", "Usage-based billing for teams over 50 seats"), b("bullet", "Kill the legacy export pipeline"), b("h2", "Owners"), b("todo", "Confirm onboarding owner with Mara", true), b("todo", "Draft billing RFC by Nov 3"), b("todo", "Get export deprecation date into the changelog"), ], }, { id: "n3", title: "Weekend sourdough", folderId: "recipes", updatedAt: "2026-10-22T08:05:00", blocks: [ b("p", "500g flour (80% bread, 20% whole wheat), 375g water, 100g starter, 10g salt."), b("h2", "Schedule"), b("todo", "Fri 9pm: feed starter", true), b("todo", "Sat 9am: mix, rest 1 hour", true), b("todo", "Sat 10am to 2pm: four sets of folds", true), b("todo", "Shape, then cold proof overnight"), b("todo", "Sun 8am: bake 250°C, 20 min lid on, 25 off"), ], }, { id: "n4", title: "1:1 with Mara", folderId: "work", updatedAt: "2026-10-21T11:30:00", blocks: [ b("h2", "Her topics"), b("bullet", "Wants more ownership of the design system"), b("bullet", "Feeling stretched across two squads"), b("h2", "Mine"), b("bullet", "Promo packet timeline: draft due before the end of the month"), b("todo", "Move her off the billing squad after the RFC lands"), ], }, { id: "n5", title: "Gift ideas", folderId: "personal", updatedAt: "2026-10-18T19:12:00", blocks: [ b("todo", "Dad: the hiking poles he keeps borrowing"), b("todo", "Ines: pottery class voucher", true), b("todo", "Sam: that out-of-print photo book (check secondhand)"), b("p", "Budget around €60 each. Order before Dec 10 for shipping."), ], }, { id: "n6", title: "Reading list", folderId: "personal", updatedAt: "2026-10-12T22:40:00", blocks: [ b("p", "Slowly working through the pile on the nightstand."), b("bullet", "The one about lighthouse keepers Jo recommended"), b("bullet", "Something short and funny for the flight"), b("bullet", "Finish the essay collection (stuck on chapter 6)"), ], }, { id: "n7", title: "Apartment move", folderId: "personal", updatedAt: "2026-09-30T10:00:00", blocks: [ b("todo", "Give notice to landlord", true), b("todo", "Book van for the 2nd", true), b("todo", "Forward mail"), b("todo", "Measure the alcove for the bookshelf"), ], }, ]; function relDate(iso: string, nowIso: string) { const d = new Date(iso); const now = new Date(nowIso); const day = (x: Date) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime(); const diff = Math.round((day(now) - day(d)) / 86400000); const hm = `${d.getHours() % 12 || 12}:${String(d.getMinutes()).padStart(2, "0")} ${d.getHours() < 12 ? "AM" : "PM"}`; if (diff <= 0) return { short: hm, long: `today at ${hm}` }; if (diff === 1) return { short: "Yesterday", long: `yesterday at ${hm}` }; if (diff < 7) { const wd = d.toLocaleDateString("en-US", { weekday: "long" }); return { short: wd, long: `${wd} at ${hm}` }; } const md = d.toLocaleDateString("en-US", { month: "short", day: "numeric" }); return { short: md, long: md }; } function preview(n: Note) { return n.blocks .filter((x) => x.text.trim()) .map((x) => x.text) .join(" · "); } function todoStats(n: Note) { const todos = n.blocks.filter((x) => x.type === "todo"); return { done: todos.filter((x) => x.done).length, total: todos.length }; } function Highlight({ text, query }: { text: string; query: string }) { const q = query.trim(); if (!q) return <>{text}; const i = text.toLowerCase().indexOf(q.toLowerCase()); if (i < 0) return <>{text}; return ( <> {text.slice(0, i)} {text.slice(i, i + q.length)} {text.slice(i + q.length)} ); } function excerpt(text: string, query: string) { const q = query.trim().toLowerCase(); if (!q) return text; const i = text.toLowerCase().indexOf(q); if (i < 30) return text; const from = text.lastIndexOf(" ", i - 16); return `…${text.slice(from > 0 ? from + 1 : i - 16)}`; } function ProgressRing({ done, total }: { done: number; total: number }) { const r = 5.5; const c = 2 * Math.PI * r; const reduce = useReducedMotionSafe(); return ( ); } function AutoText({ value, className, inputRef, ...props }: TextareaHTMLAttributes & { value: string; inputRef?: (el: HTMLTextAreaElement | null) => void }) { const local = useRef(null); useLayoutEffect(() => { const el = local.current; if (!el || (typeof CSS !== "undefined" && CSS.supports("field-sizing", "content"))) return; el.style.height = "auto"; el.style.height = `${el.scrollHeight}px`; }, [value]); return (