{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "notes",
  "type": "registry:component",
  "title": "Notes",
  "description": "Tufte-style numbered margin notes (CSS-counter ordered, tap-to-expand below 1280px) and dotted-underline term popovers with viewport-edge flip.",
  "dependencies": [],
  "registryDependencies": [
    "https://mocoui.site/r/tokens.json"
  ],
  "files": [
    {
      "path": "registry/notes/notes.tsx",
      "type": "registry:component",
      "target": "components/moco/notes.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport \"./notes.css\";\n\n/**\n * Tufte-style margin note. Numbering comes from a CSS counter, so it is always\n * document order with no registration bookkeeping. On screens ≥1280px it sits\n * in the right gutter; below that it collapses to a tap-to-expand disclosure.\n * There is no jump-to-bottom footnote anywhere.\n *\n * Wrap your prose column in `.moco-notes-scope` — it initializes the counter\n * and is the positioned ancestor the margin notes hang off.\n */\nexport interface NoteProps {\n  children: React.ReactNode;\n}\n\n/* Margin notes are absolutely positioned at their anchor's line, so two notes\n   anchored close together would overlap in the gutter. After layout, walk the\n   scope's notes in document order and push each below the previous one.\n   Idempotent: safe to run from every note on mount, resize, or font load. */\nfunction relayoutScope(scope: Element) {\n  const notes = scope.querySelectorAll<HTMLElement>(\".moco-sidenote-wrap\");\n  let prevBottom = -Infinity;\n  notes.forEach((n) => {\n    n.style.removeProperty(\"--moco-note-shift\");\n    const r = n.getBoundingClientRect();\n    const shift = Math.max(0, prevBottom + 10 - r.top);\n    if (shift > 0) n.style.setProperty(\"--moco-note-shift\", `${shift}px`);\n    prevBottom = Math.max(prevBottom, r.top + shift + r.height);\n  });\n}\n\nfunction useNoteLayout(ref: React.RefObject<HTMLElement | null>) {\n  React.useLayoutEffect(() => {\n    const scope = ref.current?.closest(\".moco-notes-scope\");\n    if (!scope) return;\n    let raf = 0;\n    const schedule = () => {\n      cancelAnimationFrame(raf);\n      raf = requestAnimationFrame(() => relayoutScope(scope));\n    };\n    schedule();\n    document.fonts?.ready.then(schedule).catch(() => {});\n    window.addEventListener(\"resize\", schedule);\n    return () => {\n      cancelAnimationFrame(raf);\n      window.removeEventListener(\"resize\", schedule);\n    };\n  }, [ref]);\n}\n\nexport function Note({ children }: NoteProps) {\n  const [open, setOpen] = React.useState(false);\n  const id = React.useId();\n  const wrap = React.useRef<HTMLSpanElement>(null);\n  useNoteLayout(wrap);\n  return (\n    <span className=\"moco-note\">\n      <span className=\"moco-noteref\" aria-hidden=\"true\" />\n      <span className=\"moco-sidenote-wrap\" ref={wrap}>\n        <button\n          type=\"button\"\n          className=\"moco-sidenote-toggle\"\n          aria-expanded={open}\n          aria-controls={id}\n          onClick={() => setOpen((v) => !v)}\n        >\n          <span className=\"moco-sidenote-num\" /> {open ? \"hide note\" : \"note\"}\n        </button>\n        <span id={id} className=\"moco-sidenote moco-sidenote-body\" data-open={open}>\n          <span className=\"moco-sidenote-num moco-desktop-only\" aria-hidden=\"true\" />\n          {children}\n        </span>\n      </span>\n    </span>\n  );\n}\n\n/**\n * Dotted-underline term revealing a definition in a popover.\n *\n * The popover anchors left by default and flips to anchor right when the term\n * sits in the right half of the viewport, so it can never run off the edge.\n * CSS alone can't do this — it doesn't know where the inline box landed.\n */\nexport interface TermProps {\n  children: React.ReactNode;\n  def: React.ReactNode;\n}\n\nexport function Term({ children, def }: TermProps) {\n  const id = React.useId();\n  const ref = React.useRef<HTMLSpanElement>(null);\n  const [flip, setFlip] = React.useState(false);\n\n  const measure = () => {\n    const r = ref.current?.getBoundingClientRect();\n    if (r) setFlip(r.left + r.width / 2 > window.innerWidth / 2);\n  };\n\n  return (\n    <span\n      ref={ref}\n      className=\"moco-term\"\n      tabIndex={0}\n      aria-describedby={id}\n      onPointerEnter={measure}\n      onFocus={measure}\n    >\n      {children}\n      <span className=\"moco-term-pop\" id={id} role=\"tooltip\" data-flip={flip}>\n        {def}\n      </span>\n    </span>\n  );\n}\n"
    },
    {
      "path": "registry/notes/notes.css",
      "type": "registry:file",
      "target": "components/moco/notes.css",
      "content": "/* ── notes scope ─────────────────────────────────────────────────────────────\n   Positioned, because margin notes hang off its right edge. Wrap the prose\n   column in this — it holds the note counter and is the only reliable\n   ancestor when a renderer emits headings and paragraphs as flat siblings. */\n.moco-notes-scope {\n  position: relative;\n  counter-reset: moco-note;\n}\n\n/* ── sidenotes ─────────────────────────────────────────────────────────── */\n\n.moco-note {\n  counter-increment: moco-note;\n}\n\n.moco-noteref {\n  color: var(--moco-accent-dk);\n  font-size: 0.72em;\n  vertical-align: super;\n  line-height: 0;\n  padding: 0 0.1em;\n}\n\n.moco-noteref::after {\n  content: counter(moco-note);\n}\n\n.moco-sidenote-num::before {\n  content: counter(moco-note);\n  color: var(--moco-accent-dk);\n  font-size: 0.8em;\n  vertical-align: super;\n  line-height: 0;\n  margin-right: 0.25em;\n}\n\n.moco-sidenote {\n  font-family: var(--moco-serif);\n  font-size: 14px;\n  line-height: 1.6;\n  color: var(--moco-muted);\n}\n\n/* The inline disclosure is the default everywhere — it needs no room and no\n   positioned ancestor, so it can't escape whatever contains it. */\n.moco-sidenote-wrap {\n  display: block;\n  margin: 0.25rem 0 1rem;\n}\n\n.moco-sidenote-body {\n  display: block;\n  border-left: 1px solid var(--moco-line);\n  padding-left: 0.9rem;\n  margin-top: 0.5rem;\n}\n\n.moco-sidenote-body[data-open=\"false\"] {\n  display: none;\n}\n\n.moco-sidenote-body .moco-desktop-only {\n  display: none;\n}\n\n.moco-sidenote-toggle {\n  font: inherit;\n  font-family: var(--moco-mono);\n  font-size: 12px;\n  color: var(--moco-faint);\n  background: none;\n  border: none;\n  border-bottom: 1px dotted var(--moco-line);\n  padding: 0;\n  cursor: pointer;\n}\n\n/* The true margin note is an upgrade: it needs the prose column's right\n   gutter, which only `.moco-notes-scope` provides. Outside it there is\n   nothing to hang off, and the note would anchor to the viewport. */\n@media (min-width: 1280px) {\n  .moco-notes-scope .moco-sidenote-wrap {\n    position: absolute;\n    left: calc(100% + 2rem);\n    width: 240px;\n    /* --moco-note-shift is set by the component's collision pass, pushing a\n       note below its predecessor when two anchors sit close together. */\n    margin: calc(-0.4rem + var(--moco-note-shift, 0px)) 0 0;\n  }\n  .moco-notes-scope .moco-sidenote-toggle {\n    display: none;\n  }\n  .moco-notes-scope .moco-sidenote-body {\n    display: block;\n    border-left: 0;\n    padding-left: 0;\n    margin-top: 0;\n  }\n  .moco-notes-scope .moco-sidenote-body .moco-desktop-only {\n    display: inline;\n  }\n}\n\n/* With JS off the toggle can't fire, so never hide the note. Add `moco-no-js`\n   to <html> (and remove it from an inline script) to opt in. */\n.moco-no-js .moco-sidenote-body[data-open=\"false\"] {\n  display: block;\n}\n\n/* ── term tooltip ──────────────────────────────────────────────────────── */\n\n.moco-term {\n  position: relative;\n  border-bottom: 1px dotted var(--moco-faint);\n  cursor: help;\n}\n\n.moco-term-pop {\n  position: absolute;\n  bottom: calc(100% + 8px);\n  left: 0;\n  z-index: 20;\n  width: max-content;\n  max-width: min(280px, 70vw);\n  background: var(--moco-bg);\n  border: 1px solid var(--moco-line);\n  border-radius: 2px;\n  padding: 0.55rem 0.7rem;\n  font-size: 12.5px;\n  line-height: 1.5;\n  color: var(--moco-muted);\n  opacity: 0;\n  pointer-events: none;\n  transition: opacity 150ms var(--moco-ease);\n}\n\n.moco-term-pop[data-flip=\"true\"] {\n  left: auto;\n  right: 0;\n}\n\n.moco-term:hover .moco-term-pop,\n.moco-term:focus-visible .moco-term-pop,\n.moco-term:focus-within .moco-term-pop {\n  opacity: 1;\n}\n\n/* On narrow screens an anchored popover can't be kept on screen without JS,\n   so the definition becomes an inline disclosure instead — the same treatment\n   sidenotes get at this width. */\n@media (max-width: 1023px) {\n  .moco-term-pop {\n    position: static;\n    display: none;\n    opacity: 1;\n    max-width: 100%;\n    margin: 0.4rem 0;\n  }\n  .moco-term:hover .moco-term-pop,\n  .moco-term:focus-within .moco-term-pop {\n    display: inline-block;\n  }\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .moco-term-pop {\n    transition-duration: 0.001ms;\n  }\n}\n"
    }
  ],
  "docs": "\"use client\";\n\nimport * as React from \"react\";\nimport { Note, Term } from \"./notes\";\n\nexport default function NotesDemo() {\n  return (\n    <div className=\"moco-notes-scope\" style={{ maxWidth: 640, margin: \"0 auto\" }}>\n      <p>\n        Margin notes number themselves in document order\n        <Note>\n          This is a margin note. On wide screens it sits in the right gutter;\n          on narrow ones it becomes this tap-to-expand disclosure.\n        </Note>\n        and a{\" \"}\n        <Term def=\"A short inline definition that appears on hover or keyboard focus, flipping sides near the viewport edge.\">\n          term\n        </Term>{\" \"}\n        reveals its definition without leaving the sentence.\n      </p>\n      <p>\n        A second note\n        <Note>Counters need no registration — this one is “2” automatically.</Note>{\" \"}\n        keeps counting on its own.\n      </p>\n    </div>\n  );\n}\n",
  "meta": {
    "tags": [
      "prose",
      "sidenote",
      "tooltip",
      "typography"
    ]
  }
}