{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "calligram",
  "title": "Calligram",
  "description": "A paragraph that fills a shape, as Apollinaire's calligrams did: each line is as wide as the shape is at that height, so the outline is only implied. Or it falls as rain, or runs round a frame about one word.",
  "dependencies": [
    "@chenglou/pretext"
  ],
  "registryDependencies": [
    "https://thedirectors.agency/ui/r/0nlytype.json"
  ],
  "files": [
    {
      "path": "registry/0nlytype/ui/calligram.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 attributes on <html> that can change the face. Not class: smooth scrolling toggles one on every scroll. */\nconst FACE = [\"data-pair\", \"data-scheme\", \"data-mode\", \"data-key\"]\n\ntype Shape = \"circle\" | \"fermata\" | \"wave\"\ntype Variant = \"fill\" | \"rain\" | \"mirror\"\n\ntype CalligramProps = Omit<React.ComponentProps<\"p\">, \"children\"> & {\n  /** The paragraph. Plain text: pretext measures it. */\n  children: string\n  /** circle is the 0 of 0nlyType, fermata an arc over a dot, wave a sound wave. For fill only. */\n  shape?: Shape\n  /** fill: the paragraph fills the shape. rain: it falls in streaks of letters, as in Apollinaire's \"Il pleut\".\n   *  mirror: it runs round the four sides of a frame and spirals in, around `centre`, as in his \"Cœur couronne et miroir\". */\n  variant?: Variant\n  /** For mirror: the word in the middle, set large in the expression italic. */\n  centre?: string\n  /** The shape's diameter (its full width) as a CSS length. Default: the width it's given, up to 34rem. */\n  size?: string\n  /** Let the colour follow the shape: ink at its centre, pencil at its edge. */\n  fade?: boolean\n}\n\ntype Side = { text: string; spacing: number; x: number; y: number; a: number; len: number }\ntype Streak = { letters: string[]; x: number; y: number }\ntype Laid = {\n  lines: { text: string; spacing: number; tracking: number; t: number }[]\n  size: number\n  lh: number\n  top: number\n  height: number\n  sides?: Side[]\n  centre?: number\n  streaks?: Streak[]\n}\n\nconst REF = 100 // pretext measures once at this size; widths scale straight from it\nconst WAVE = { rows: 11, low: 0.34 } // one swell every 11 rows, from a third of the width to all of it\nconst RAIN = { pitch: 2.2, drop: 0.8, slant: 0.16, stagger: 9 } // ems between streaks and between letters; the lean a letter; the most a streak starts late, in letters\n\n/** The same streak always starts at the same height: a small hash of its index. */\nconst rand = (i: number) => {\n  const s = Math.sin(i * 12.9898 + 78.233) * 43758.5453\n  return s - Math.floor(s)\n}\n\n/** Rain: the words in streaks of at most `most` letters, a word never split unless it is longer than a streak. */\nfunction streaks(words: string[], most: number) {\n  const out: string[][] = []\n  let run: string[] = []\n  for (const word of words) {\n    const letters = [...new Intl.Segmenter(undefined, { granularity: \"grapheme\" }).segment(word)].map((g) => g.segment)\n    if (run.length && run.length + 1 + letters.length > most) {\n      out.push(run)\n      run = []\n    }\n    if (run.length) run.push(\" \")\n    run.push(...letters)\n    while (run.length > most) out.push(run.splice(0, most))\n  }\n  if (run.length) out.push(run)\n  return out\n}\n\n/**\n * A paragraph that fills a shape, as Apollinaire's calligrams did. Each line's width is the shape's\n * chord at that line's height, and the line sits centred in it, spread to reach both sides, so the\n * outline is only implied by the text. The circle and the fermata's half-disc are filled exactly:\n * the size of the type is found so the last word lands in the last row. Pointing shows nothing new.\n * Until the fonts and the layout are ready it is a plain paragraph.\n */\nfunction Calligram({ children: text, shape = \"circle\", variant = \"fill\", centre, size, fade = false, className, style, ref: forwardedRef, ...props }: CalligramProps) {\n  const ref = React.useRef<HTMLParagraphElement>(null)\n  const composedRef = useComposedRefs(ref, forwardedRef)\n  const middle = React.useRef<HTMLSpanElement>(null)\n  const [laid, setLaid] = React.useState<Laid | null>(null)\n\n  React.useEffect(() => {\n    const el = ref.current\n    if (!el) return\n    let cancelled = false\n    const off: (() => void)[] = []\n\n    ;(async () => {\n      let lib: typeof import(\"@chenglou/pretext\")\n      try {\n        lib = await import(\"@chenglou/pretext\")\n      } catch {\n        return // the plain paragraph stays\n      }\n      let face = \"\", prepared: ReturnType<typeof lib.prepareWithSegments> | undefined, longest = 0\n\n      // The room a row has: the shape's chord across it, from its top edge y0 to its bottom edge y1.\n      function chord(D: number, i: number, y0: number, y1: number) {\n        if (shape === \"wave\") return D * (1 - (1 - WAVE.low) * (1 + Math.cos((2 * Math.PI * i) / WAVE.rows)) / 2)\n        const r = D / 2\n        const far = shape === \"fermata\" ? r - y0 : Math.max(Math.abs(y0 - r), Math.abs(y1 - r)) // from the centre, or the base\n        return far >= r ? 0 : 2 * Math.sqrt(r * r - far * far)\n      }\n\n      // Set the text at size s in a shape D wide; null if it doesn't all fit and `force` is off.\n      function set(s: number, D: number, ratio: number, force: boolean): Laid | null {\n        const p = prepared!, k = s / REF, lh = ratio * s\n        const H = shape === \"circle\" ? D : D / 2\n        const rows = shape === \"wave\" ? 1e4 : Math.floor(H / lh)\n        const y0 = shape === \"wave\" ? 0 : (H - rows * lh) / 2\n        const lines: Laid[\"lines\"] = []\n        let cursor = { segmentIndex: 0, graphemeIndex: 0 }, first = -1, done = false, i = 0\n        for (; !done && (i < rows || force); i++) {\n          const w = chord(D, Math.min(i, rows - 1), y0 + i * lh, y0 + (i + 1) * lh)\n          if (w < longest * k * 1.02) { if (first < 0 || i >= rows) continue; break } // too narrow for the longest word: leave the row\n          const want = w * 0.985\n          // Greedy fit, unless one more word fits by squeezing the gaps a little: the closer, the better.\n          let line = lib.layoutNextLine(p, cursor, want / k)\n          if (!line) { done = true; break }\n          if (first < 0) first = i\n          const more = lib.layoutNextLine(p, cursor, (want * 1.06) / k)\n          if (more && more.end.segmentIndex > line.end.segmentIndex) {\n            const g = more.text.trimEnd().split(\" \").length - 1\n            if (g && (more.width * k - want) / g <= s * 0.06) line = more\n          }\n          const words = line.text.trimEnd()\n          const gaps = words.split(\" \").length - 1\n          // Spread the line to its chord, mostly in the gaps and a little in the letters; the last line stays as it is.\n          const last = !lib.layoutNextLine(p, line.end, 1e5)\n          const slack = last ? 0 : want - line.width * k\n          const spacing = gaps ? Math.max(-s * 0.06, Math.min(slack / gaps, s * 0.3)) : 0\n          const rest = slack - spacing * gaps\n          const tracking = rest > 0 ? Math.min(rest / words.length, s * 0.03) : 0\n          lines.push({ text: words, spacing, tracking, t: 1 - w / D })\n          cursor = line.end\n        }\n        if (!done && lib.layoutNextLine(p, cursor, 1e5)) return null\n        const top = shape === \"wave\" ? 0 : y0 + first * lh\n        return { lines, size: s, lh, top, height: shape === \"wave\" ? lines.length * lh : H }\n      }\n\n      // Rain: streaks a pitch apart, each letter a drop below the last and leaning a little, each starting a little late.\n      function rain(D: number, base: number, rtl: boolean): Laid {\n        const words = text.split(/\\s+/).filter(Boolean)\n        const pitch = RAIN.pitch * base, lean = RAIN.slant * base\n        let most = 6, laid: string[][] = []\n        for (; most < 400; most++) {\n          laid = streaks(words, most)\n          if (laid.length * pitch + most * lean <= D) break\n        }\n        const used = laid.length * pitch + most * lean, lh = RAIN.drop * base\n        const from = (D - used) / 2 + most * lean\n        const out = laid.map((letters, i) => {\n          const x = from + i * pitch\n          return { letters, x: rtl ? D - x - pitch : x, y: Math.floor(rand(i) * RAIN.stagger) * lh }\n        })\n        const height = Math.max(...out.map((s) => s.y + s.letters.length * lh))\n        return { lines: [], size: base, lh, top: 0, height, streaks: out }\n      }\n\n      // Mirror: one line to each side of the frame, clockwise from the top, each ring a line inside the last, round\n      // until the words run out. Each side is spread to its length but the last.\n      function mirror(D: number, base: number, ratio: number): Laid {\n        const p = prepared!, k = base / REF, lh = base * ratio\n        const sides: Side[] = []\n        let cursor = { segmentIndex: 0, graphemeIndex: 0 }, d = 0\n        for (let ring = 0; ring < 40 && D - 2 * d > 4 * lh; ring++, d += lh) {\n          const four = [\n            { x: d, y: d, a: 0, len: D - 2 * d },\n            { x: D - d, y: d + lh, a: 90, len: D - 2 * d - lh },\n            { x: D - d - lh, y: D - d, a: 180, len: D - 2 * d - lh },\n            { x: d, y: D - d - lh, a: 270, len: D - 2 * d - 2 * lh },\n          ]\n          for (const side of four) {\n            const line = lib.layoutNextLine(p, cursor, side.len / k)\n            if (!line) break\n            const words = line.text.trimEnd()\n            const gaps = words.split(\" \").length - 1\n            const last = !lib.layoutNextLine(p, line.end, 1e5)\n            const spacing = gaps && !last ? Math.min((side.len - line.width * k) / gaps, base * 0.6) : 0\n            sides.push({ text: words, spacing, ...side })\n            cursor = line.end\n          }\n          if (!lib.layoutNextLine(p, cursor, 1e5)) { d += lh; break }\n        }\n        // The word in the middle: as large as the room inside the last ring allows, at most the loudest size.\n        let big = 0\n        const mid = middle.current\n        if (mid && centre) {\n          const ms = getComputedStyle(mid)\n          const w = lib.measureNaturalWidth(lib.prepareWithSegments(centre, `${ms.fontStyle} ${ms.fontWeight} ${REF}px ${ms.fontFamily}`))\n          const room = D - 2 * d - 2 * lh\n          big = Math.max(base, Math.min((room * 0.8 * REF) / w, room * 0.6, base * 6))\n        }\n        return { lines: [], size: base, lh, top: 0, height: D, sides, centre: big }\n      }\n\n      async function lay() {\n        if (cancelled || !el!.isConnected) return\n        const style = getComputedStyle(el!)\n        const base = parseFloat(style.fontSize)\n        if (variant === \"rain\") {\n          if (!cancelled) setLaid(rain(el!.clientWidth, base, style.direction === \"rtl\"))\n          return\n        }\n        if (variant === \"mirror\" && middle.current) await document.fonts.load(`italic 400 ${REF}px ${getComputedStyle(middle.current).fontFamily}`, centre)\n        if (cancelled || !el!.isConnected) return\n        const next = `${style.fontStyle} ${style.fontWeight} ${REF}px ${style.fontFamily}`\n        if (next !== face || !prepared) {\n          face = next\n          await document.fonts.load(face, text)\n          if (cancelled || !el!.isConnected) return\n          prepared = lib.prepareWithSegments(text, face)\n          longest = Math.max(...prepared.widths)\n        }\n        const ratio = parseFloat(style.lineHeight) / base || 1.4\n        const D = el!.clientWidth\n        let result: Laid | null\n        if (variant === \"mirror\") result = mirror(D, base, ratio)\n        else if (shape === \"wave\") result = set(base, D, ratio, true)\n        else {\n          // The largest type that still holds all the words: a search, since widths only shrink with size.\n          let lo = 7, hi = base * 2\n          result = null\n          for (let n = 0; n < 12; n++) {\n            const mid = (lo + hi) / 2\n            const fit = set(mid, D, ratio, false)\n            if (fit) { result = fit; lo = mid } else hi = mid\n          }\n          result ??= set(lo, D, ratio, true) // too much text for this size: it runs on below the shape\n        }\n        if (!cancelled) setLaid(result)\n      }\n\n      await lay()\n      if (cancelled) return\n      let width = el.clientWidth\n      const resized = new ResizeObserver(() => {\n        if (el.clientWidth !== width) { width = el.clientWidth; lay() }\n      })\n      resized.observe(el)\n      // A change of pair or scheme on <html> can change the face: lay out again.\n      const restyled = new MutationObserver(() => lay())\n      restyled.observe(document.documentElement, { attributeFilter: FACE })\n      off.push(() => { resized.disconnect(); restyled.disconnect() })\n    })()\n\n    return () => {\n      cancelled = true\n      off.forEach((f) => f())\n    }\n  }, [text, shape, variant, centre])\n\n  return (\n    <p ref={composedRef} data-slot=\"calligram\" data-shape={variant === \"fill\" ? shape : undefined} data-variant={variant === \"fill\" ? undefined : variant} data-fade={fade || undefined} className={cn(\"ot-calligram\", className)} style={{ ...(size ? { \"--size\": size } : null), ...style } as React.CSSProperties} {...props}>\n      <span className=\"ot-sr\">{text}</span>\n      <span\n        aria-hidden=\"true\"\n        className=\"ot-calligram-lines\"\n        data-laid={laid ? \"\" : undefined}\n        style={laid ? { fontSize: laid.size, lineHeight: `${laid.lh}px`, paddingTop: laid.top, height: laid.height } : undefined}\n      >\n        {!laid\n          ? text\n          : laid.streaks\n            ? laid.streaks.map((streak, i) => (\n                <span key={i} className=\"ot-calligram-streak\" style={{ left: streak.x, top: streak.y }}>\n                  {streak.letters.map((g, j) => (\n                    <span key={j} style={{ \"--j\": j, \"--t\": 1 - j / Math.max(1, streak.letters.length - 1) } as React.CSSProperties}>{g}</span>\n                  ))}\n                </span>\n              ))\n            : laid.sides\n              ? laid.sides.map((side, i) => (\n                  <span key={i} className=\"ot-calligram-side\" style={{ \"--x\": `${side.x}px`, \"--a\": `${side.a}deg`, \"--t\": i / laid.sides!.length, top: side.y, width: side.len, wordSpacing: side.spacing } as React.CSSProperties}>{side.text}</span>\n                ))\n              : laid.lines.map((line, i) => (\n                  <span key={i} style={{ \"--t\": line.t, wordSpacing: line.spacing, letterSpacing: line.tracking } as React.CSSProperties}>{line.text}</span>\n                ))}\n      </span>\n      {variant === \"mirror\" && centre ? (\n        <span ref={middle} className=\"ot-calligram-centre\" style={laid?.centre ? { fontSize: laid.centre } : undefined}>\n          {centre}\n        </span>\n      ) : null}\n      {variant === \"fill\" && shape === \"fermata\" ? <span aria-hidden=\"true\" className=\"ot-calligram-dot\" /> : null}\n    </p>\n  )\n}\n\nexport { Calligram, type CalligramProps }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/0nlytype/styles/calligram.css",
      "content": "@layer components {\n  /* ── ot-calligram ── a paragraph that fills a shape. There is no outline: the lines are laid to the\n     shape's width at their height, centred, and that is the whole drawing. The circle and the\n     fermata's half-disc have a height of their own, so nothing moves when the lines arrive. */\n  /* Its width never comes from its lines: they are laid to the width, so a width that followed them would lay them again, narrower, for ever.\n     In a box that fits its content it asks for --size. */\n  .ot-calligram { --size: 34rem; width: min(100%, var(--size)); max-width: 100%; margin: 0; font-size: var(--ot-p); line-height: 1.4; contain: inline-size; contain-intrinsic-inline-size: var(--size); }\n  .ot-calligram-lines { display: block; box-sizing: border-box; text-align: center; color: var(--ot-ink); }\n  .ot-calligram-lines > span { display: block; white-space: pre; }\n  .ot-calligram[data-shape=\"circle\"] .ot-calligram-lines { aspect-ratio: 1; }\n  .ot-calligram[data-shape=\"fermata\"] .ot-calligram-lines { aspect-ratio: 2; }\n  /* Before the layout, the plain text waits inside the shape's box. */\n  .ot-calligram-lines:not([data-laid]) { display: grid; place-content: center; overflow: hidden; font-size: var(--ot-pp); line-height: var(--ot-pp-lh); }\n  :is(.ot-calligram[data-shape=\"wave\"], .ot-calligram[data-variant=\"rain\"]) .ot-calligram-lines:not([data-laid]) { display: block; }\n  .ot-calligram[data-fade] .ot-calligram-lines > span { color: color-mix(in oklab, var(--ot-ink) calc((1 - var(--t)) * 100%), var(--ot-pencil)); }\n  /* rain: after Apollinaire's \"Il pleut\". The words fall in streaks, a letter to a drop, each letter a sixth of an em\n     further over than the one above; the streaks start at different heights, as rain does. With data-fade each\n     streak's letters darken as they fall, the drop at its foot in ink. */\n  .ot-calligram[data-variant=\"rain\"] .ot-calligram-lines[data-laid] { position: relative; }\n  .ot-calligram-streak { position: absolute; display: block; }\n  .ot-calligram-streak > span { display: block; width: 1em; line-height: 0.8em; /* a drop: RAIN.drop */ text-align: center; white-space: pre; translate: calc(var(--j) * -0.16em) 0; }\n  :is([dir=\"rtl\"], [dir=\"rtl\"] *) .ot-calligram-streak > span { translate: calc(var(--j) * 0.16em) 0; }\n  .ot-calligram[data-fade] .ot-calligram-streak > span { color: color-mix(in oklab, var(--ot-ink) calc((1 - var(--t)) * 100%), var(--ot-pencil)); }\n  /* mirror: after Apollinaire's \"Cœur couronne et miroir\". The paragraph is the frame: a line to each side, clockwise\n     from the top and turned to face out, each ring a line inside the last, until the words run out. What is left in the\n     middle holds one word in the expression italic, as the mirror held his name. Right to left it runs the other way. */\n  .ot-calligram[data-variant=\"mirror\"] { position: relative; }\n  .ot-calligram[data-variant=\"mirror\"] .ot-calligram-lines { aspect-ratio: 1; position: relative; }\n  .ot-calligram-side { position: absolute; inset-inline-start: var(--x); display: block; white-space: pre; text-align: start; transform-origin: 0 0; rotate: var(--a); }\n  :is([dir=\"rtl\"], [dir=\"rtl\"] *) .ot-calligram-side { transform-origin: 100% 0; rotate: calc(-1 * var(--a)); }\n  .ot-calligram[data-fade] .ot-calligram-side { color: color-mix(in oklab, var(--ot-ink) calc((1 - var(--t)) * 100%), var(--ot-pencil)); }\n  .ot-calligram-centre {\n    position: absolute; inset: 0; display: grid; place-content: center; pointer-events: none;\n    font-family: var(--ot-expression); font-style: italic; font-weight: 400; line-height: 1; color: var(--ot-ink); white-space: nowrap;\n  }\n  .ot-calligram[data-variant=\"mirror\"] .ot-calligram-lines:not([data-laid]) ~ .ot-calligram-centre { position: static; display: block; font-size: var(--ot-mf); }\n  /* The fermata's dot, under the middle of the arc. */\n  .ot-calligram-dot { display: block; width: var(--ot-dot); height: var(--ot-dot); margin: var(--ot-space-4) auto 0; border-radius: 50%; background: var(--ot-accent); }\n  @media (forced-colors: active) { .ot-calligram-dot { background: CanvasText; } }\n}\n",
      "type": "registry:file",
      "target": "styles/0nlytype/calligram.css"
    }
  ],
  "css": {
    "@import \"../styles/0nlytype/calligram.css\"": {}
  },
  "meta": {
    "movement": "II",
    "underneath": "hook",
    "contract": "ot-calligram"
  },
  "type": "registry:ui"
}
