{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "melody",
  "title": "Melody",
  "description": "A sentence set on a stave, each word a note. Point at one and the phrase plays from it; or sing along discs on the staff; or draw the staff only where there are words.",
  "dependencies": [
    "@chenglou/pretext"
  ],
  "registryDependencies": [
    "https://thedirectors.agency/ui/r/0nlytype.json"
  ],
  "files": [
    {
      "path": "registry/0nlytype/ui/melody.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { useComposedRefs } from \"@/lib/0nlytype/refs\"\nimport { cn } from \"@/lib/utils\"\n\n/** The <html> switches that change the face; smooth scrolling toggles a class there on every scroll, which must not re-lay the text. */\nconst FACE = [\"data-pair\", \"data-scheme\", \"data-mode\", \"data-key\"]\n\n/** A word: its place across the stave, its width, its step, and (for the noteheads) its baseline under the staff. */\ntype Note = { text: string; i: number; x: number; w: number; step: number; y?: number }\n/** One stave's engraving, as SVG path data from the top line: the slurs, the fermata, and where the dynamic stands. */\ntype Marks = { slurs: string; hold: string; dynamic?: { x: number; y: number } }\ntype Layout = {\n  staves: Note[][]\n  marks: Marks[]\n  space: number // one staff space, in px\n  above: number // room over the top line, for what rises past it\n  height: number // one stave\n  close: number // where the last stave ends, just past its last word\n  dynamic: string // the dynamic the type size is: pp, p, mp, mf, f or ff\n  rtl: boolean\n  width: number\n  base: number // where a word's baseline falls in its own box, in px\n}\n\ntype MelodyProps = Omit<React.ComponentProps<\"figure\">, \"children\"> & {\n  /** The sentence, as plain text: pretext measures each word. */\n  children: string\n  /** The staff step of each word: 0 is the bottom line, 2 the next, 8 the top, odd numbers the spaces between. */\n  contour?: number[]\n  /** stave: the words are the notes. noteheads: the notes are discs on the staff and the words are sung under it. cutaway: the staff is drawn only under the words. */\n  variant?: \"stave\" | \"noteheads\" | \"cutaway\"\n}\n\nconst clamp = (n: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, n))\n\n/** A tune from the sentence itself: long words rise, the phrase rocks, and it comes home to where it began. */\nfunction tune(words: string[]) {\n  const len = words.map((w) => w.replace(/[^\\p{L}\\p{N}]/gu, \"\").length || 1)\n  const seed = len.reduce((a, b) => a + b, 0)\n  const mean = seed / len.length\n  const steps = len.map((l, i) => clamp(Math.round(4 + 2.4 * Math.sin(i * 0.75 + seed * 0.9) + (l - mean) * 0.5), 0, 8))\n  steps[steps.length - 1] = steps[0]\n  return steps\n}\n\n/**\n * A sentence set on a stave, each word a note. Pretext measures the words, so they're spaced evenly\n * across the measure and wrap onto a new stave when it's full. Pointing at a word sounds it and the\n * rest of the phrase, in turn. The whole figure takes focus and Enter plays it from the start; a\n * reader of the page gets the plain sentence.\n * `noteheads` sets the tune as discs on the staff, the words underlaid as a score's lyrics, and\n * sings along them as the calendar poster marks its days: sung in ink, here in the accent, to come\n * in rings. `cutaway` draws the staff only under the words, as a cutaway score drops an instrument's\n * lines while it rests.\n */\nfunction Melody({ children: text, contour, variant = \"stave\", className, style, onKeyDown, onBlur, onPointerLeave, ref: forwardedRef, ...props }: MelodyProps) {\n  const ref = React.useRef<HTMLElement>(null)\n  const composedRef = useComposedRefs(ref, forwardedRef)\n  const [layout, setLayout] = React.useState<Layout | null>(null)\n  // noteheads: the word you're at, and the one you were at, so the discs fill or empty in turn from there.\n  const [pos, setPos] = React.useState({ at: -1, from: -1 })\n  const move = (at: number) => setPos((p) => (p.at === at ? p : { at, from: p.at }))\n  const tuneKey = contour?.join()\n  const heads = variant === \"noteheads\"\n\n  React.useEffect(() => {\n    const el = ref.current\n    if (!el) return\n    let cancelled = false\n    let font = \"\"\n    let width = 0\n    const off: (() => void)[] = []\n    const words = text.split(/\\s+/).filter(Boolean)\n    const own = tune(words)\n    const steps = words.map((_, i) => clamp(contour?.[i] ?? own[i], -4, 12))\n\n    ;(async () => {\n      let lib: typeof import(\"@chenglou/pretext\")\n      try {\n        lib = await import(\"@chenglou/pretext\")\n      } catch {\n        return // the plain sentence stays\n      }\n\n      async function lay() {\n        if (cancelled || !el!.isConnected) return\n        const style = getComputedStyle(el!)\n        const next = `${style.fontStyle} ${style.fontWeight} ${style.fontSize} ${style.fontFamily}`\n        if (next !== font) {\n          font = next\n          await document.fonts.load(font, text)\n        }\n        width = el!.clientWidth\n        if (cancelled || !width) return\n        const size = parseFloat(style.fontSize)\n        const letterSpacing = parseFloat(style.letterSpacing) || 0 // \"normal\" is 0\n        const w = words.map((word) => lib.measureNaturalWidth(lib.prepareWithSegments(word, font, { letterSpacing })))\n\n        // Where the baseline sits in a line-height:1 box: the font's ascent, once the box has centred the font's height.\n        const metrics = document.createElement(\"canvas\").getContext(\"2d\")!\n        metrics.font = font\n        const m = metrics.measureText(\"x\")\n        const base = m.fontBoundingBoxAscent === undefined ? size * 0.8 : (size - (m.fontBoundingBoxAscent + m.fontBoundingBoxDescent)) / 2 + m.fontBoundingBoxAscent\n\n        // A staff space is an even number of px, so the hairlines stay crisp and a step is a whole px.\n        const space = 2 * Math.round(size * 0.35)\n        let above = Math.round(size * 0.85) + clamp(Math.max(...steps) - 8, 0, 4) * (space / 2)\n        let below = Math.round(size * 0.35) + Math.max(0, -Math.min(...steps)) * (space / 2)\n        const edge = size * 0.7 // the barlines' room\n        const room = width - 2 * edge\n        const least = size * 0.6\n\n        // Fill a stave, then spread its words evenly across it. The last one keeps to a moderate gap: a rest, not a stretch.\n        const rows: number[][] = []\n        let row: number[] = []\n        let used = 0\n        words.forEach((_, i) => {\n          if (row.length && used + least + w[i] > room) {\n            rows.push(row)\n            row = []\n            used = 0\n          }\n          used += (row.length ? least : 0) + w[i]\n          row.push(i)\n        })\n        if (row.length) rows.push(row)\n        // No word alone on the last stave: it takes one from the stave before, if that one can spare it.\n        const [prev, last] = rows.slice(-2)\n        if (last?.length === 1 && prev.length > 2 && w[prev.at(-1)!] + least + w[last[0]] <= room) last.unshift(prev.pop()!)\n        const staves = rows.map((idx, r) => {\n          const sum = idx.reduce((a, i) => a + w[i], 0)\n          const even = idx.length > 1 ? (room - sum) / (idx.length - 1) : 0\n          const gap = r === rows.length - 1 ? Math.min(even, least * 2.2) : even\n          let x = edge\n          return idx.map((i) => {\n            const note: Note = { text: words[i], i, x, w: w[i], step: steps[i] }\n            x += w[i] + gap\n            return note\n          })\n        })\n        // Engraving, in the marks a singer reads: a slur over each phrase (the words up to a pause: a comma, a full stop), a pen\n        // stroke that swells in the middle and thins to nothing at its ends; a fermata over the last word, where the tune holds;\n        // and under the first word its dynamic, in the italic: the type size it's set at is a dynamic already (pp, p, mp, mf, f, ff).\n        // y runs down from the top line; a word's baseline is its step.\n        const lineY = (step: number) => (4 - step / 2) * space\n        const r = (v: number) => Math.round(v * 10) / 10\n        const cap = size * 0.74 // a word's top over its baseline, with the ascenders\n        const gap = size * 0.2\n        const swell = Math.max(1.5, size * 0.07) // the slur's thickness at its middle\n        /** A tapered arc from (x0, y0) to (x1, y1): a cubic whose handles rise h1 and h2 over the chord, and an inner one t\n         *  lower at the middle, so the stroke swells to t and thins to nothing at its ends. */\n        const arc = (x0: number, y0: number, x1: number, y1: number, h1: number, h2: number, t: number) => {\n          const [ax, bx, ay, by] = [x0 + (x1 - x0) / 3, x0 + (2 * (x1 - x0)) / 3, y0 + (y1 - y0) / 3, y0 + (2 * (y1 - y0)) / 3]\n          const d = (4 * t) / 3 // lowering both handles by d lowers the middle by t\n          return `M${r(x0)} ${r(y0)}C${r(ax)} ${r(ay - h1)} ${r(bx)} ${r(by - h2)} ${r(x1)} ${r(y1)}C${r(bx)} ${r(by - h2 + d)} ${r(ax)} ${r(ay - h1 + d)} ${r(x0)} ${r(y0)}Z`\n        }\n        const dot = (x: number, y: number, d: number) => `M${r(x - d)} ${r(y)}a${r(d)} ${r(d)} 0 1 0 ${r(2 * d)} 0a${r(d)} ${r(d)} 0 1 0 ${r(-2 * d)} 0Z`\n        const top0 = (n: Note) => lineY(n.step) - cap - gap\n        let top = 0\n        let bottom = 4 * space\n        const marks = staves.map((notes, row): Marks => {\n          if (heads) {\n            // The words are the lyric line, one baseline under the staff and under the lowest disc; no slurs, since every\n            // word is one note. The fermata stands over the last disc, and the dynamic goes above the staff, over the first,\n            // where a vocal score puts it to leave the words below clear.\n            const low = Math.max(4 * space, ...notes.map((n) => lineY(n.step) + space / 2))\n            const lyric = low + size * 1.05\n            notes.forEach((n) => (n.y = lyric))\n            top = Math.min(top, ...notes.map((n) => lineY(n.step) - space / 2 - 2))\n            bottom = Math.max(bottom, lyric + size * 0.3)\n            let hold = \"\"\n            if (row === staves.length - 1) {\n              const z = notes[notes.length - 1]\n              const cx = z.x + z.w / 2\n              const half = size * 0.42\n              const base = Math.min(-space * 0.8, lineY(z.step) - space / 2 - gap)\n              hold = arc(cx - half, base, cx + half, base, size * 0.53, size * 0.53, swell * 1.3) + dot(cx, base - size * 0.1, Math.max(1.5, size * 0.075))\n              top = Math.min(top, base - size * 0.4)\n            }\n            let dynamic: { x: number; y: number } | undefined\n            if (row === 0) {\n              const a = notes[0]\n              const y = Math.min(-space * 0.6, lineY(a.step) - space / 2 - gap)\n              dynamic = { x: a.x, y }\n              top = Math.min(top, y - size * 0.75)\n            }\n            return { slurs: \"\", hold, dynamic }\n          }\n          const phrases: Note[][] = [[]]\n          notes.forEach((n, k) => {\n            phrases[phrases.length - 1].push(n)\n            if (/[,.;:!?]$/.test(n.text) && k < notes.length - 1) phrases.push([])\n          })\n          let slurs = \"\"\n          let under: ((x: number) => number) | undefined // the top of the slur that ends on the stave's last word, for the fermata to clear\n          for (const ph of phrases) {\n            if (ph.length < 2) continue // a slur joins two notes at least\n            const [a, z] = [ph[0], ph[ph.length - 1]]\n            // From over the first word to over the last: their middles, or, where the tune falls away from the first (or climbs to\n            // the last), their inner ends, so the slur doesn't have to hook back over the rest of a high word to leave it.\n            const x0 = a.x + w[a.i] * (a.step > ph[1].step ? 0.85 : 0.5)\n            const x1 = z.x + w[z.i] * (z.step > ph[ph.length - 2].step ? 0.15 : 0.5)\n            let [y0, y1] = [top0(a), top0(z)]\n            // The slur's underside must clear every word it passes over. Its height over the chord at u is\n            // 3u(1-u)((1-u)h1 + u h2), so each word asks one straight line of (h1, h2); take the lowest pair that satisfies\n            // them all, so the slur leans toward whichever end needs the lift, as an engraver's would. Past a sensible bow,\n            // raise its ends instead.\n            const bow = () => {\n              const asks = ph.flatMap((n) =>\n                [n.x, n.x + w[n.i] / 2, n.x + w[n.i]].map((x) => {\n                  const u = clamp((x - x0) / (x1 - x0), 0.1, 0.9)\n                  const [p, q] = [3 * u * (1 - u) * (1 - u), 3 * u * u * (1 - u)]\n                  return { p, q, need: y0 + (y1 - y0) * u - top0(n) + (p + q) * ((4 * swell) / 3) }\n                }),\n              )\n              let best = [size, size, Infinity]\n              for (let h1 = 0; h1 <= size * 6; h1 += size * 0.05) {\n                const h2 = Math.max(size * 0.2, ...asks.map((a) => (a.need - a.p * h1) / a.q))\n                const cost = h1 * h1 + h2 * h2 // the evenest bow that clears them\n                if (h1 >= size * 0.2 && cost < best[2]) best = [h1, h2, cost]\n              }\n              return best\n            }\n            let [h1, h2] = bow()\n            const most = Math.max(size * 1.8, (x1 - x0) * 0.16) // a long phrase may bow higher\n            for (let tries = 0; Math.max(h1, h2) > most && tries < 40; tries++) {\n              ;[y0, y1] = [y0 - size * 0.1, y1 - size * 0.1]\n              ;[h1, h2] = bow()\n            }\n            slurs += arc(x0, y0, x1, y1, h1, h2, swell)\n            top = Math.min(top, Math.min(y0, y1) - 0.75 * Math.max(h1, h2))\n            if (z === notes[notes.length - 1]) {\n              const [c0, c1, c2, c3] = [y0, y0 + (y1 - y0) / 3 - h1, y0 + (2 * (y1 - y0)) / 3 - h2, y1]\n              under = (x) => {\n                const t = clamp((x - x0) / (x1 - x0), 0, 1)\n                return (1 - t) ** 3 * c0 + 3 * (1 - t) ** 2 * t * c1 + 3 * (1 - t) * t * t * c2 + t ** 3 * c3\n              }\n            }\n          }\n          let hold = \"\"\n          if (row === staves.length - 1) {\n            const z = notes[notes.length - 1]\n            const cx = z.x + w[z.i] / 2\n            const half = size * 0.42\n            const slur = under ? Math.min(...[-1, -0.5, 0, 0.5, 1].map((f) => under!(cx + f * half))) - gap : Infinity\n            const base = Math.min(-space * 0.8, top0(z), slur) // above the staff, the word and the slur under it\n            hold = arc(cx - half, base, cx + half, base, size * 0.53, size * 0.53, swell * 1.3) + dot(cx, base - size * 0.1, Math.max(1.5, size * 0.075))\n            top = Math.min(top, base - size * 0.4)\n          }\n          let dynamic: { x: number; y: number } | undefined\n          if (row === 0) {\n            const a = notes[0]\n            const y = Math.max(4 * space + size * 0.95, lineY(a.step) + size * 0.95) // under the staff, and under the word's descenders\n            dynamic = { x: a.x, y }\n            bottom = Math.max(bottom, y + size * 0.1)\n          }\n          return { slurs, hold, dynamic }\n        })\n        above = Math.max(above, Math.ceil(-top + 3))\n        below = Math.max(below, Math.ceil(bottom - 4 * space + 3))\n        // A short last line ends where the sentence does, as a score's last system is left short rather than stretched.\n        const end = staves[staves.length - 1].at(-1)!\n        const close = Math.min(width, Math.ceil(end.x + w[end.i] + edge))\n        const root = getComputedStyle(document.documentElement)\n        const rem = parseFloat(root.fontSize)\n        const dynamic = [\"pp\", \"p\", \"mp\", \"mf\", \"f\", \"ff\"]\n          .map((d) => [d, Math.abs(parseFloat(root.getPropertyValue(`--ot-${d}`)) * rem - size)] as const)\n          .reduce((a, b) => (b[1] < a[1] ? b : a))[0]\n        setLayout({ staves, marks, space, above, height: above + 4 * space + below, base, close, dynamic, rtl: style.direction === \"rtl\", width })\n      }\n\n      await lay()\n      if (cancelled) return\n      const resized = new ResizeObserver(() => {\n        if (el.clientWidth !== width) lay()\n      })\n      resized.observe(el)\n      off.push(() => resized.disconnect())\n      // A change of pair or scheme on <html> can change the face: lay out again if it did.\n      const restyled = new MutationObserver(() => lay())\n      restyled.observe(document.documentElement, { attributeFilter: FACE })\n      off.push(() => restyled.disconnect())\n    })()\n\n    return () => {\n      cancelled = true\n      off.forEach((f) => f())\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps -- `contour` is watched by its contents\n  }, [text, tuneKey, heads])\n\n  /** Sound word `from` and the ones after it: CSS lights each in turn, one arpeggio step apart. */\n  const play = (from: number) => {\n    const notes = ref.current?.querySelectorAll<HTMLElement>(\".ot-melody-note\")\n    if (!notes) return\n    notes.forEach((n) => delete n.dataset.lit)\n    void ref.current!.offsetWidth // so that the same word, sounded again, starts over\n    notes.forEach((n, i) => {\n      if (i < from) return\n      n.style.setProperty(\"--k\", String(i - from))\n      n.dataset.lit = \"\"\n    })\n  }\n  const hush = () => ref.current?.querySelectorAll<HTMLElement>(\".ot-melody-note\").forEach((n) => delete n.dataset.lit)\n  // Without motion the sounded words hold their colour, and let it go when you leave.\n  const still = () => matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n\n  return (\n    <figure\n      ref={composedRef}\n      data-slot=\"melody\"\n      tabIndex={0}\n      aria-label={text}\n      className={cn(\"ot-melody\", className)}\n      style={layout ? ({ ...style, \"--s\": `${layout.space}px`, \"--b\": `${layout.base}px` } as React.CSSProperties) : style}\n      {...props}\n      data-variant={variant}\n      onKeyDown={(e) => {\n        onKeyDown?.(e)\n        if (e.target !== e.currentTarget) return\n        if (!heads) {\n          if (e.key === \"Enter\") play(0)\n          return\n        }\n        // Noteheads: Enter sings it through; the arrows step along the words (the way they run), Home and End go to the\n        // first and the last, Escape lets it go.\n        const last = text.split(/\\s+/).filter(Boolean).length - 1\n        const on = layout?.rtl ? -1 : 1\n        const to: Record<string, number> = { Enter: last, ArrowRight: pos.at + on, ArrowLeft: pos.at - on, Home: 0, End: last, Escape: -1 }\n        if (!(e.key in to)) return\n        e.preventDefault()\n        move(Math.max(e.key === \"Escape\" ? -1 : 0, Math.min(last, to[e.key])))\n      }}\n      onBlur={(e) => {\n        onBlur?.(e)\n        if (heads) move(-1)\n        else if (still()) hush()\n      }}\n      onPointerLeave={(e) => {\n        onPointerLeave?.(e)\n        if (heads) move(-1)\n        else if (still()) hush()\n      }}\n    >\n      {layout ? (\n        <div aria-hidden=\"true\" className=\"ot-melody-staves\">\n          {layout.staves.map((notes, r) => (\n            <div key={r} className=\"ot-melody-stave\" data-last={r === layout.staves.length - 1 || undefined} style={{ blockSize: layout.height }}>\n              <i className=\"ot-melody-staff\" style={{ insetBlockStart: layout.above, inlineSize: r === layout.staves.length - 1 ? layout.close : undefined }} />\n              <svg className=\"ot-melody-marks\">\n                {/* In right-to-left the notes run from the right: the strokes are mirrored to follow them; the dynamic is set, not mirrored, and starts from its word's start either way. */}\n                <g transform={layout.rtl ? `translate(${layout.width} ${layout.above}) scale(-1 1)` : `translate(0 ${layout.above})`}>\n                  <path d={layout.marks[r].slurs + layout.marks[r].hold} />\n                </g>\n                {layout.marks[r].dynamic ? (\n                  <text\n                    className=\"ot-melody-dynamic\"\n                    x={layout.rtl ? layout.width - layout.marks[r].dynamic.x : layout.marks[r].dynamic.x}\n                    y={layout.above + layout.marks[r].dynamic.y}\n                  >\n                    {layout.dynamic}\n                  </text>\n                ) : null}\n              </svg>\n              {variant === \"cutaway\"\n                ? notes.map((n) => (\n                    <i key={n.i} className=\"ot-melody-bit\" style={{ insetInlineStart: n.x, inlineSize: n.w, insetBlockStart: layout.above }} />\n                  ))\n                : null}\n              {heads\n                ? notes.map((n) => (\n                    <i\n                      key={n.i}\n                      className=\"ot-melody-head\"\n                      data-sung={pos.at > n.i || undefined}\n                      data-here={pos.at === n.i || undefined}\n                      style={{ insetInlineStart: n.x + n.w / 2, insetBlockStart: layout.above + (4 - n.step / 2) * layout.space, \"--k\": Math.abs(n.i - Math.max(0, pos.from)) } as React.CSSProperties}\n                      onPointerEnter={() => move(n.i)}\n                    />\n                  ))\n                : null}\n              {notes.map((n) => (\n                <span\n                  key={n.i}\n                  className=\"ot-melody-note\"\n                  data-hold={r === layout.staves.length - 1 && n === notes[notes.length - 1] ? \"\" : undefined}\n                  style={{ insetInlineStart: n.x, insetBlockStart: layout.above + (n.y ?? (4 - n.step / 2) * layout.space) }}\n                  onPointerEnter={() => (heads ? move(n.i) : play(n.i))}\n                >\n                  {n.text}\n                </span>\n              ))}\n            </div>\n          ))}\n        </div>\n      ) : (\n        <span aria-hidden=\"true\">{text}</span>\n      )}\n    </figure>\n  )\n}\n\nexport { Melody, type MelodyProps }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/0nlytype/styles/melody.css",
      "content": "@layer components {\n  /* ── ot-melody ── a sentence on a stave: five hairlines, and each word sits on a line or in a\n     space as a note does. Pretext spaces the words across the measure; pointing at one sounds it,\n     and the rest of the phrase lights after it in turn, then settles back. */\n  .ot-melody { margin: 0; font-size: var(--ot-mp); line-height: var(--ot-mp-lh); letter-spacing: var(--ot-mp-tr); color: var(--ot-ink); }\n  .ot-melody-stave { position: relative; margin-block-end: var(--ot-space-5); }\n  .ot-melody-stave:last-child { margin-block-end: 0; }\n  /* The staff: five hairlines a space apart, the opening barline, and a barline to close each stave. */\n  .ot-melody-staff {\n    position: absolute; inset-inline: 0; block-size: calc(4 * var(--s) + var(--ot-hairline));\n    border-inline-start: var(--ot-hairline) solid var(--ot-rule-strong);\n    background: repeating-linear-gradient(to bottom, var(--ot-rule) 0 var(--ot-hairline), transparent var(--ot-hairline) var(--s));\n  }\n  .ot-melody-staff::before, .ot-melody-staff::after {\n    content: \"\"; position: absolute; inset-block: 0; inset-inline-end: 0; background: var(--ot-rule-strong);\n  }\n  .ot-melody-staff::before { inline-size: var(--ot-hairline); }\n  .ot-melody-staff::after { display: none; }\n  /* The last stave ends on a double bar, thin then heavy. */\n  .ot-melody-stave[data-last] .ot-melody-staff::before { inset-inline-end: calc(var(--ot-stroke) + 3px); }\n  .ot-melody-stave[data-last] .ot-melody-staff::after { display: block; inline-size: var(--ot-stroke); }\n  /* The engraving: slurs and the fermata, drawn as pen strokes (filled, so they swell and taper), and the dynamic in the italic,\n     all in pencil, under the words. In right-to-left the script mirrors the strokes to follow the notes. */\n  .ot-melody-marks { position: absolute; inset: 0; inline-size: 100%; block-size: 100%; overflow: visible; pointer-events: none; fill: var(--ot-pencil); }\n  .ot-melody-dynamic { font-family: var(--ot-expression); font-style: italic; font-weight: 600; font-size: calc(var(--ot-expression-scale) * 0.82em); letter-spacing: 0.02em; }\n  /* A note: the word's baseline sits on the step it was given. A halo of the ground around its letters stops the staff lines\n     and the stems just short of them, as a score leaves the lines clear of the words: the stave stays whole, and the words read.\n     Set --ot-melody-ground if the melody sits on something other than the paper. */\n  .ot-melody-note {\n    position: absolute; line-height: 1; white-space: pre; translate: 0 calc(-1 * var(--b));\n    paint-order: stroke fill; -webkit-text-stroke: 0.22em var(--ot-melody-ground, var(--ot-paper));\n  }\n  .ot-melody-note[data-lit] {\n    animation: ot-melody-sound var(--ot-adagio) var(--ot-exhale) calc(var(--k) * var(--ot-arpeggio));\n  }\n  /* Under its fermata the last word holds: it keeps the accent for as long again before it lets go. */\n  .ot-melody-note[data-lit][data-hold] { animation-name: ot-melody-hold; animation-duration: calc(2 * var(--ot-adagio)); }\n  /* Sounded: the accent, a small lift that lands, then it settles back to ink. */\n  @keyframes ot-melody-sound {\n    0%   { color: var(--ot-accent); translate: 0 calc(-1 * var(--b) - 0.14em); }\n    12%  { color: var(--ot-accent); translate: 0 calc(-1 * var(--b)); }\n    35%  { color: var(--ot-accent); }\n    100% { color: var(--ot-ink); }\n  }\n  @keyframes ot-melody-hold {\n    0%   { color: var(--ot-accent); translate: 0 calc(-1 * var(--b) - 0.14em); }\n    6%   { color: var(--ot-accent); translate: 0 calc(-1 * var(--b)); }\n    68%  { color: var(--ot-accent); }\n    100% { color: var(--ot-ink); }\n  }\n  /* Without motion there's no travel, only the colour: it holds until you leave. */\n  @media (prefers-reduced-motion: reduce) {\n    :is(.ot-melody-note[data-lit], .ot-melody-note[data-lit][data-hold]) { animation: none; color: var(--ot-accent); }\n  }\n\n  /* noteheads: the tune as discs on the staff, one a staff space across, the words underlaid as a\n     score's lyrics; no stems, no slurs. The discs speak as the calendar poster's days do: to come in\n     hairline rings, sung in ink, the word you're at in the accent. They fill (or empty) in turn from\n     where you were, one arpeggio apart. The fermata stands over the last disc; the dynamic above the\n     staff, as a vocal score sets it. */\n  .ot-melody-head {\n    position: absolute; inline-size: var(--s); block-size: var(--s); margin-inline-start: calc(var(--s) / -2); margin-block-start: calc(var(--s) / -2);\n    border-radius: 50%; border: var(--ot-stroke) solid var(--ot-rule-strong); background: var(--ot-melody-ground, var(--ot-paper));\n    transition: background-color var(--ot-moderato) var(--ot-exhale), border-color var(--ot-moderato) var(--ot-exhale); transition-delay: calc(var(--k) * var(--ot-arpeggio));\n  }\n  .ot-melody-head[data-sung] { background-color: var(--ot-ink); border-color: var(--ot-ink); }\n  .ot-melody-head[data-here] { background-color: var(--ot-accent); border-color: var(--ot-accent); }\n  .ot-melody[data-variant=\"noteheads\"] .ot-melody-note { cursor: default; }\n\n  /* cutaway: as a cutaway score drops an instrument's staff while it rests, the five lines are drawn\n     only under the words; between them, and at the ends, there is only the paper, and the slurs carry\n     across the silence. No barlines. */\n  .ot-melody[data-variant=\"cutaway\"] .ot-melody-staff { display: none; }\n  .ot-melody-bit {\n    position: absolute; block-size: calc(4 * var(--s) + var(--ot-hairline)); margin-inline-start: -0.12em; padding-inline: 0.12em; box-sizing: content-box;\n    background: repeating-linear-gradient(to bottom, var(--ot-rule) 0 var(--ot-hairline), transparent var(--ot-hairline) var(--s));\n  }\n\n  @media (forced-colors: active) {\n    .ot-melody-note { -webkit-text-stroke: 0; } /* the halo would be forced to the text colour and blot the words */\n    .ot-melody-head { forced-color-adjust: none; border-color: CanvasText; background: Canvas; }\n    .ot-melody-head[data-sung] { background: CanvasText; }\n    .ot-melody-head[data-here] { background: Highlight; border-color: Highlight; }\n    .ot-melody-bit, .ot-melody-staff { forced-color-adjust: none; background-image: repeating-linear-gradient(to bottom, GrayText 0 var(--ot-hairline), transparent var(--ot-hairline) var(--s)); }\n  }\n}\n",
      "type": "registry:file",
      "target": "styles/0nlytype/melody.css"
    }
  ],
  "css": {
    "@import \"../styles/0nlytype/melody.css\"": {}
  },
  "meta": {
    "movement": "II",
    "underneath": "hook",
    "contract": "ot-melody"
  },
  "type": "registry:ui"
}
