Book a call

Folded note

An index card standing on your page, creased in the middle, with its top half as a head. It nods while idle, stands up to listen, tips back and dog-ears its corner while it thinks, and writes on its own lines while it works.

When it's done it ticks itself off. When something breaks it crumples and slumps. It's drawn in outline, so it sits in a calm interface without shouting.

Install with AI

Paste this into Claude Code, Cursor or any AI coding tool.

Add the Folded note component from https://baked.design/interfaces/paper-note.md to this project. Follow the install steps on that page and use the code exactly as written.

Install by hand

Copy this file into your project.

paper-note.js
// Folded note: an AI agent folded from an index card, by Baked Design Studio (baked.design/interfaces).
// A card standing on your page, creased in the middle. The top half is its head: it nods while idle, stands up
// to listen, tips back and dog-ears its corner while it thinks, writes on its own lines while it works, ticks
// itself when it's done and crumples when something breaks.
//
//   import { mount } from "./paper-note.js";
//   const note = mount(document.querySelector("#agent"));
//   note.set("working");              // idle · listening · thinking · working · done · error
//   note.hear();                      // call on each keystroke while listening
//   note.tune({ patience: 0.2 });     // 0 to 1: patience, confidence, chattiness
//
// Lines take the text colour (currentColor); the paper is --note-paper (your page colour by default).
// No dependencies.

const STATES = ["idle", "listening", "thinking", "working", "done", "error"];
const NSV = "http://www.w3.org/2000/svg";
const mk = (tag, attrs = {}) => { const e = document.createElementNS(NSV, tag); for (const k in attrs) e.setAttribute(k, attrs[k]); return e; };
const W = 62, H = 58, D = Math.PI / 180;
const LINES = [16, 30, 44];

// a joined-up scribble along one ruled line, so "writing" looks like handwriting rather than a bar
function scribble(y, seed) {
  let d = `M-46,${y}`;
  for (let x = -46; x <= 44; x += 3) {
    const w = Math.sin(x * 0.45 + seed) * 3.2 + Math.sin(x * 0.17 + seed * 2) * 1.5;
    d += `L${(x + Math.cos(x * 0.45 + seed) * 1.6).toFixed(1)},${(y - 3 + w).toFixed(1)}`;
  }
  return d;
}

export function mount(el, options = {}) {
  const p = { patience: 0.5, confidence: 0.5, chattiness: 0.5, ...options };
  const still = matchMedia("(prefers-reduced-motion: reduce)");
  const svg = mk("svg", { viewBox: "-86 -72 172 144", "aria-hidden": "true" });
  Object.assign(svg.style, { width: "100%", height: "100%", display: "block", overflow: "visible" });
  const line = { fill: "none", stroke: "currentColor", "stroke-width": 1.6, "stroke-linejoin": "round", "stroke-linecap": "round" };
  const paper = { ...line, fill: "var(--note-paper, Canvas)" };
  const ground = mk("path", { ...line, d: `M-84,${H + 2}H84`, "stroke-opacity": 0.25 });
  const back = mk("polygon", paper), ear = mk("polygon", paper), crease = mk("path", { ...line, "stroke-opacity": 0.5 });
  const rules = LINES.map((y) => mk("path", { ...line, d: `M-48,${y}H48`, "stroke-opacity": 0.22, "stroke-width": 1 }));
  const words = LINES.map((y, i) => mk("path", { ...line, d: scribble(y, i * 2.3 + 1), "stroke-width": 1.4 }));
  const tick = mk("path", { ...line, d: "M-16,28 L-4,42 L22,12", "stroke-width": 3.2 });
  const flap = mk("polygon", paper);
  const eyes = [-1, 1].map(() => { const g = mk("g"); const slit = mk("rect", { fill: "currentColor" }); const arc = mk("path", { ...line, "stroke-width": 2.4, d: "M-6,2 Q0,-5 6,2" }); const x = mk("path", { ...line, "stroke-width": 2.2, d: "M-4,-4L4,4M4,-4L-4,4" }); g.append(slit, arc, x); return { g, slit, arc, x }; });
  svg.append(ground, back, ...rules, ...words, tick, ear, crease, flap, ...eyes.map((e) => e.g));
  el.append(svg);
  const lens = words.map((w) => w.getTotalLength?.() || 140), tickLen = tick.getTotalLength?.() || 50;

  let state = STATES.includes(options.state) ? options.state : "idle";
  let t = 0, last = 0, raf = 0, seen = true, heard = 0, since = 0, lastMove = performance.now();
  const cur = { tilt: 14, ear: 0, crumple: 0, ink: 0, tick: 0, open: 1, look: 0 };

  function goal() {
    const s = 1.45 - p.patience * 0.8, amp = 0.6 + p.chattiness * 0.8;
    const bored = (performance.now() - lastMove) / 1000 > 3 + 8 * (1 - p.chattiness);
    switch (state) {
      case "listening": return { tilt: -4 + heard * 10 * Math.sin(t * 30), ear: 0, crumple: 0, ink: 0, tick: 0, open: 1.3, look: 0 };
      case "thinking": return { tilt: -24 + 6 * Math.sin(t * 1.4 * s), ear: 0.5 + 0.5 * Math.sin(t * 2 * s), crumple: 0, ink: 0, tick: 0, open: 0.8, look: -1 };
      case "working": return { tilt: 12 + 9 * Math.abs(Math.sin(t * 3 * s)) * amp, ear: 0, crumple: 0, ink: (t * 0.45 * s) % 3.6, tick: 0, open: 0.7, look: 1 };
      case "done": return { tilt: 6 + 14 * Math.max(0, Math.sin(since * 9)) * Math.max(0, 1 - since * 0.8), ear: 0, crumple: 0, ink: 3, tick: Math.min(1, since * 2), open: 1, look: 0 };
      case "error": return { tilt: 38, ear: 0.2, crumple: 1, ink: 1.4, tick: 0, open: 1, look: 1 };
      default: return { tilt: 14 + 4 * Math.sin(t * 1.2 * s) * amp + (bored ? 10 * Math.max(0, Math.sin(t * 0.9)) : 0), ear: 0, crumple: 0, ink: 0, tick: 0, open: 1, look: 0 };
    }
  }
  const jit = (x, y, k) => cur.crumple ? [x + Math.sin(x * 0.7 + y * 1.3 + k) * 3.5 * cur.crumple, y + Math.cos(x * 1.1 - y * 0.6 + k) * 3 * cur.crumple] : [x, y];
  const pts = (arr) => arr.map(([x, y], i) => jit(x, y, i).map((v) => v.toFixed(2)).join(",")).join(" ");
  function draw() {
    const th = cur.tilt * D, c = Math.cos(th), sn = Math.sin(th);
    const shake = state === "error" ? Math.sin(t * 40) * 3 * Math.max(0, 1 - since * 1.5) : 0;
    svg.style.transform = shake ? `translateX(${shake.toFixed(2)}px)` : "";
    // the bottom half, with its corner folded over by `ear`
    const f = 16 * cur.ear;
    back.setAttribute("points", pts([[-W, 0], [W, 0], [W, H - f], [W - f, H], [-W, H]]));
    ear.setAttribute("points", pts([[W, H - f], [W - f, H - f], [W - f, H]]));
    ear.style.display = f > 0.5 ? "" : "none";
    crease.setAttribute("d", cur.crumple > 0.05 ? `M-${W},22 L-30,30 L-8,18 L14,34 L36,24 L${W},32` : "M0,0");
    crease.style.opacity = cur.crumple;
    // writing: each line fills in turn
    words.forEach((w, i) => { const k = Math.max(0, Math.min(1, cur.ink - i)); w.style.strokeDasharray = lens[i]; w.style.strokeDashoffset = (lens[i] * (1 - k)).toFixed(1); w.style.opacity = k > 0.01 ? 1 - cur.tick * 0.6 : 0; });
    tick.style.strokeDasharray = tickLen; tick.style.strokeDashoffset = (tickLen * (1 - cur.tick)).toFixed(1); tick.style.opacity = cur.tick > 0.01 ? 1 : 0;
    // the top half: hinged at the crease, foreshortened as it tips, a little wider when it leans toward you
    const top = -H * c, half = W * (1 + 0.16 * sn);
    flap.setAttribute("points", pts([[-W, 0], [W, 0], [half, top], [-half, top]]));
    const face = c > 0.25;
    eyes.forEach(({ g, slit, arc, x }, i) => {
      g.style.display = face ? "" : "none"; if (!face) return;
      const ey = top * 0.55 + cur.look * 3 * c, ex = (i ? 1 : -1) * 20 * (1 + 0.16 * sn * 0.55);
      const blink = state !== "error" && state !== "done" && (t * (1.1 - p.patience * 0.6)) % 3.6 < 0.1;
      const h = Math.max(1.2, 11 * c * cur.open * (blink ? 0.15 : 1));
      slit.setAttribute("x", -2.6); slit.setAttribute("width", 5.2); slit.setAttribute("y", (-h / 2).toFixed(2)); slit.setAttribute("height", h.toFixed(2)); slit.setAttribute("rx", 2.6);
      slit.style.display = state === "done" || state === "error" ? "none" : "";
      arc.style.display = state === "done" ? "" : "none"; x.style.display = state === "error" ? "" : "none";
      g.setAttribute("transform", `translate(${ex.toFixed(2)},${ey.toFixed(2)}) scale(1,${Math.max(0.3, c).toFixed(2)})`);
    });
  }
  function frame(now) {
    raf = 0;
    const dt = Math.min(0.05, (now - (last || now)) / 1000); last = now;
    t += dt; since += dt; heard = Math.max(0, heard - dt * 2.5);
    const g = goal(), fk = 1 - Math.exp(-dt * (4 + p.confidence * 10));
    for (const k in g) cur[k] += (g[k] - cur[k]) * (k === "ink" && g.ink < cur.ink - 1 ? 1 : fk);
    draw();
    if (seen && !document.hidden && !still.matches) raf = requestAnimationFrame(frame);
  }
  const wake = () => { if (!raf) { last = 0; raf = requestAnimationFrame(frame); } };
  const settle = () => { since = 9; Object.assign(cur, goal()); if (state === "working") cur.ink = 2; draw(); };

  const onMove = () => { lastMove = performance.now(); };
  addEventListener("pointermove", onMove, { passive: true });
  const io = new IntersectionObserver(([e]) => { seen = e.isIntersecting; if (seen) wake(); });
  io.observe(el);
  const onVis = () => !document.hidden && wake();
  document.addEventListener("visibilitychange", onVis);

  settle(); wake();
  return {
    get state() { return state; },
    set(next) { if (!STATES.includes(next) || next === state) return; state = next; since = 0; if (state === "working") cur.ink = 0; if (still.matches) settle(); else wake(); },
    hear() { heard = Math.min(1, heard + 0.5); lastMove = performance.now(); wake(); },
    tune(next) { Object.assign(p, next); if (still.matches) settle(); else wake(); },
    destroy() { cancelAnimationFrame(raf); io.disconnect(); removeEventListener("pointermove", onMove); document.removeEventListener("visibilitychange", onVis); svg.remove(); },
  };
}

Use

Example
import { mount } from "./paper-note.js";

const note = mount(document.querySelector("#agent"));
note.set("working");

Notes

All interfaces