{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sign",
  "title": "Sign",
  "description": "An icon made of its own word with Pretext: ruled in middle-dot leaders, laid along its strokes, or filling its silhouette row by row. Pointed at, it says itself.",
  "dependencies": [
    "@chenglou/pretext"
  ],
  "registryDependencies": [
    "https://thedirectors.agency/ui/r/0nlytype.json"
  ],
  "files": [
    {
      "path": "registry/0nlytype/ui/sign.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { useComposedRefs } from \"@/lib/0nlytype/refs\"\nimport { cn } from \"@/lib/utils\"\n\n/** One move on the 24-unit square: move to, line to, or an arc of a circle (centre, radius, start and sweep in degrees). */\ntype Move = [\"M\", number, number] | [\"L\", number, number] | [\"O\", number, number, number, number, number]\ntype Path = Move[]\n/** A sign as data: its word, the strokes the word is set along (dots, words), and the rows it fills (fill). Each row\n *  is its centre and the runs inside the silhouette, [y, a, b, a, b…], `step` units apart. `mirror` turns it round\n *  for a right-to-left page, for a sign that points the way the reader goes. */\ntype SignShape = { word: string; line?: Path[]; fill?: { step: number; rows: number[][] }; mirror?: boolean }\ntype SignVariant = \"dots\" | \"words\" | \"fill\"\n\ntype SignProps = Omit<React.ComponentProps<\"span\">, \"children\"> & {\n  /** The sign: its word and the drawing the word is set into. */\n  shape: SignShape\n  /** dots: the strokes ruled in middle-dot leaders. words: the word runs along the strokes, ruled in leaders below\n   *  40px where letters can't hold a stroke. fill: the word fills the silhouette row by row. */\n  variant?: SignVariant\n  /** roman for what the interface offers; italic for what belongs to the person (their mail, their home). */\n  face?: \"roman\" | \"italic\"\n  /** The square's side: pixels, or a CSS length. Default 1.5rem. */\n  size?: number | string\n  /** What a reader hears. Default: the word. An empty label hides the sign from readers, for a control that already says it. */\n  label?: string\n}\n\ntype Glyph = { ch: string; x: number; y: number; r: number; k: number; say: number; hush?: boolean; quiet?: boolean }\ntype Laid = { glyphs: Glyph[]; ws: number; rest: number; said: number; word: { x: number; y: number }[] }\n/** Advances in ems, for letters in a weight. `wrap` adds to the last the pair it makes with the first, for a word that repeats. */\ntype Metrics = { advances: (letters: string[], weight: number, wrap: boolean) => number[]; leader: (weight: number) => { advance: number; ink: number } }\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\"]\nconst REF = 100 // pretext measures once at this size; widths scale straight from it\nconst WORDS = 40 // below this side the word can't hold a stroke: words are ruled in leaders, as dots are\nconst LEADER = \"·\" // the middle dot, type's own dotted line\nconst HEIGHT = 0.62 // a lowercase letter's height from descender to ascender, in ems, for telling when two touch\n\n// Advances in thousandths of an em of a–z and the middle dot, then the dot's ink, in Archivo and Bodoni Moda italic\n// at 500, 600, 700 and 800, measured in Chrome. They lay a sign out on the server and before pretext arrives, so\n// every sign renders in the first paint; pretext then sets it again with the face's real kerning.\nconst ALPHABET = \"abcdefghijklmnopqrstuvwxyz\" + LEADER\nconst TABLE: Record<\"roman\" | \"italic\", number[][]> = {\n  roman: [\n    [550, 579, 532, 579, 554, 293, 573, 573, 238, 236, 528, 238, 860, 573, 583, 579, 579, 346, 524, 305, 572, 516, 740, 529, 516, 503, 333, 113],\n    [556, 592, 547, 592, 561, 307, 591, 584, 252, 250, 543, 252, 861, 584, 598, 592, 592, 362, 541, 314, 583, 529, 758, 546, 529, 509, 333, 127],\n    [580, 608, 573, 608, 584, 325, 607, 602, 267, 264, 570, 267, 891, 602, 613, 608, 608, 380, 556, 342, 601, 547, 798, 572, 547, 519, 333, 141],\n    [616, 633, 612, 633, 619, 352, 632, 629, 288, 286, 610, 288, 937, 629, 635, 633, 633, 407, 579, 385, 629, 574, 859, 612, 574, 535, 333, 162],\n  ],\n  italic: [\n    [547, 498, 449, 553, 467, 353, 592, 562, 293, 262, 504, 290, 819, 576, 493, 552, 504, 452, 418, 328, 569, 512, 732, 521, 524, 415, 198, 137],\n    [551, 506, 461, 564, 479, 372, 600, 572, 304, 275, 519, 294, 822, 577, 506, 555, 512, 461, 431, 336, 572, 524, 754, 538, 554, 431, 198, 156],\n    [557, 518, 478, 579, 494, 397, 610, 586, 318, 293, 537, 298, 826, 579, 523, 558, 522, 472, 447, 347, 577, 539, 783, 560, 592, 451, 198, 181],\n    [563, 530, 496, 595, 511, 426, 622, 601, 334, 312, 558, 303, 830, 580, 542, 562, 533, 484, 466, 358, 582, 556, 815, 584, 635, 474, 198, 208],\n  ],\n}\n\nfunction tableMetrics(italic: boolean): Metrics {\n  const rows = TABLE[italic ? \"italic\" : \"roman\"]\n  const look = (i: number, weight: number) => {\n    const t = Math.max(0, Math.min(3, (weight - 500) / 100)), lo = Math.floor(t), hi = Math.min(3, lo + 1)\n    return (rows[lo][i] + (rows[hi][i] - rows[lo][i]) * (t - lo)) / 1000\n  }\n  return {\n    advances: (letters, weight) => letters.map((ch) => { const i = ALPHABET.indexOf(ch); return i < 0 ? 0.55 : look(i, weight) }),\n    leader: (weight) => ({ advance: look(26, weight), ink: look(27, weight) }),\n  }\n}\n\ntype Sampled = { pts: [number, number][]; along: number[]; length: number }\n\n/** A path sampled every quarter unit: its points, and how far along each one is. */\nfunction sample(path: Path): Sampled {\n  const pts: [number, number][] = []\n  for (const move of path) {\n    if (move[0] === \"M\") pts.push([move[1], move[2]])\n    else if (move[0] === \"L\") {\n      const [px, py] = pts[pts.length - 1], [, x, y] = move\n      const n = Math.max(1, Math.ceil(Math.hypot(x - px, y - py) * 4))\n      for (let i = 1; i <= n; i++) pts.push([px + ((x - px) * i) / n, py + ((y - py) * i) / n])\n    } else {\n      const [, cx, cy, r, from, sweep] = move\n      const n = Math.max(8, Math.ceil(((Math.abs(sweep) * Math.PI) / 180) * r * 4))\n      for (let i = 0; i <= n; i++) {\n        const a = ((from + (sweep * i) / n) * Math.PI) / 180\n        pts.push([cx + r * Math.cos(a), cy + r * Math.sin(a)])\n      }\n    }\n  }\n  const along = [0]\n  for (let i = 1; i < pts.length; i++) along.push(along[i - 1] + Math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1]))\n  return { pts, along, length: along[along.length - 1] }\n}\n\n/** The point s units along a sampled path, and the way it heads there, in degrees. */\nfunction at({ pts, along }: Sampled, s: number) {\n  let lo = 1, hi = along.length - 1\n  while (lo < hi) { const mid = (lo + hi) >> 1; if (along[mid] < s) lo = mid + 1; else hi = mid }\n  const i = lo\n  const [ax, ay] = pts[i - 1], [bx, by] = pts[i]\n  const t = along[i] === along[i - 1] ? 0 : (s - along[i - 1]) / (along[i] - along[i - 1])\n  return { x: ax + (bx - ax) * t, y: ay + (by - ay) * t, r: (Math.atan2(by - ay, bx - ax) * 180) / Math.PI }\n}\n\n/** The same stroke read from its other end. */\nfunction backwards(p: Sampled): Sampled {\n  const n = p.pts.length\n  return { pts: p.pts.slice().reverse(), along: p.pts.map((_, i) => p.length - p.along[n - 1 - i]), length: p.length }\n}\n\n/** For each point of a sampled stroke, how long the run it lies on is between corners: where the stroke turns more\n *  than 25° within a unit. A ring is one run all round; an arrowhead is two, one each side of its point. */\nfunction runs(p: Sampled): number[] {\n  const n = p.pts.length, heading = (i: number, j: number) => Math.atan2(p.pts[j][1] - p.pts[i][1], p.pts[j][0] - p.pts[i][0])\n  const corners = [0]\n  for (let i = 2; i < n - 2; i++) {\n    const d = Math.abs(((((heading(i, i + 2) - heading(i - 2, i)) * 180) / Math.PI + 540) % 360) - 180)\n    if (d > 25 && p.along[i] - p.along[corners[corners.length - 1]] > 0.5) corners.push(i)\n  }\n  corners.push(n - 1)\n  const out = new Array<number>(n)\n  for (let c = 0; c + 1 < corners.length; c++) for (let i = corners[c]; i <= corners[c + 1]; i++) out[i] = p.along[corners[c + 1]] - p.along[corners[c]]\n  return out\n}\n\n/** Where a stroke turns sharply (more than 50° within a unit), units along it: an arrowhead's point. */\nfunction sharp(p: Sampled): number[] {\n  const n = p.pts.length, heading = (i: number, j: number) => Math.atan2(p.pts[j][1] - p.pts[i][1], p.pts[j][0] - p.pts[i][0])\n  const out: number[] = []\n  let best = -1, most = 0\n  for (let i = 2; i < n - 2; i++) {\n    const d = Math.abs(((((heading(i, i + 2) - heading(i - 2, i)) * 180) / Math.PI + 540) % 360) - 180)\n    if (d > 50 && d > most) { best = i; most = d }\n    else if (d <= 50 && best >= 0) { out.push(p.along[best]); best = -1; most = 0 }\n  }\n  if (best >= 0) out.push(p.along[best])\n  return out\n}\n\n/** A closed stroke twice round, so a word can start anywhere on it and run on past where the drawing began. */\nfunction twice(p: Sampled): Sampled {\n  return { pts: [...p.pts, ...p.pts.slice(1)], along: [...p.along, ...p.along.slice(1).map((a) => a + p.length)], length: p.length * 2 }\n}\n\n/** The drawing turned round for a right-to-left page: mirrored, and every stroke read the other way, so it still reads forwards. */\nfunction mirrored(shape: SignShape): SignShape {\n  const path = (p: Path): Path => {\n    const out: Path = []\n    for (let i = p.length - 1; i >= 0; i--) {\n      const m = p[i]\n      if (m[0] === \"O\") out.push([\"O\", 24 - m[1], m[2], m[3], 180 - m[4] - m[5], m[5]])\n      else out.push([out.length ? \"L\" : \"M\", 24 - m[1], m[2]])\n    }\n    return out\n  }\n  return {\n    ...shape,\n    line: shape.line?.map(path),\n    fill: shape.fill && { ...shape.fill, rows: shape.fill.rows.map(([y, ...runs]) => [y, ...runs.map((x) => 24 - x).reverse()]) },\n  }\n}\n\n/** A letter's box: centre, turn and size. */\ntype Box = { x: number; y: number; r: number; w: number; h: number; stroke: number }\n\n/** How far a point is from a box, 0 inside it. */\nfunction reach(b: Box, x: number, y: number) {\n  const a = (-b.r * Math.PI) / 180, dx = x - b.x, dy = y - b.y\n  const lx = dx * Math.cos(a) - dy * Math.sin(a), ly = dx * Math.sin(a) + dy * Math.cos(a)\n  return Math.hypot(Math.max(0, Math.abs(lx) - b.w / 2), Math.max(0, Math.abs(ly) - b.h / 2))\n}\n\n/** Whether two boxes overlap: separating axes. */\nfunction touch(p: Box, q: Box) {\n  if (Math.hypot(p.x - q.x, p.y - q.y) > (Math.hypot(p.w, p.h) + Math.hypot(q.w, q.h)) / 2) return false\n  const corners = (b: Box) => {\n    const a = (b.r * Math.PI) / 180, c = Math.cos(a), s = Math.sin(a)\n    return [[-1, -1], [1, -1], [1, 1], [-1, 1]].map(([i, j]) => [b.x + (i * b.w * c) / 2 - (j * b.h * s) / 2, b.y + (i * b.w * s) / 2 + (j * b.h * c) / 2])\n  }\n  const cp = corners(p), cq = corners(q)\n  for (const b of [p, q]) for (const deg of [b.r, b.r + 90]) {\n    const ax = Math.cos((deg * Math.PI) / 180), ay = Math.sin((deg * Math.PI) / 180)\n    const span = (cs: number[][]) => { const v = cs.map(([x, y]) => x * ax + y * ay); return [Math.min(...v), Math.max(...v)] }\n    const [p0, p1] = span(cp), [q0, q1] = span(cq)\n    if (p1 <= q0 || q1 <= p0) return false\n  }\n  return true\n}\n\nconst r2 = (v: number) => Math.round(v * 100) / 100\n\n/**\n * Lays a sign out at side S in pixels. Pure: the same shape, side and metrics always give the same letters, so the\n * server and the first paint agree. Optical sizes, as a punchcutter would cut them: the smaller the sign, the\n * heavier its letters.\n */\nfunction layoutSign(shape: SignShape, variant: SignVariant, S: number, italic: boolean, m: Metrics): Laid {\n  const letters = Array.from(shape.word)\n  const t = Math.max(0, Math.min(3, Math.log2(S / 16)))\n  const rest = Math.round(800 - 100 * t) // the letters' weight in the drawing\n  const said = italic ? 500 : 560 // the weight of the word when it's said\n  const sayAdv = m.advances(letters, said, false)\n  const sayEm = sayAdv.reduce((a, b) => a + b, 0)\n  // The said word: set at a size a person reads, never under a caption, and at most half again the sign's width, so\n  // it stays the sign's own word rather than a label over its neighbours. Only a caption's floor can take it wider.\n  const ws = Math.max(11, Math.min(S * 0.42, (1.5 * S) / sayEm))\n  const u = S / 24\n  const glyphs: Glyph[] = []\n  const boxes: Box[] = []\n  let mode = variant === \"fill\" ? \"fill\" : variant === \"dots\" || S < WORDS ? \"dots\" : \"words\"\n  const lines = shape.line ?? []\n\n  /** Rules a stretch of a stroke in leaders at type size f, giving way where it meets letters already set. */\n  const leaders = (p: Sampled, from: number, to: number, f: number, stroke: number, quiet = false) => {\n    const { advance, ink } = m.leader(rest)\n    // A leader's pitch: its own advance, or half again its ink where a face sets its leaders loose (Archivo's\n    // middle dot has twice its width of air either side, Bodoni's almost none), so both faces rule a stroke as dark.\n    const pitch = Math.min(advance, ink * 1.45) * f\n    const L = to - from\n    const [a, b] = [p.pts[0], p.pts[p.pts.length - 1]]\n    const closed = from === 0 && to === p.length * u && Math.hypot(a[0] - b[0], a[1] - b[1]) < 0.01\n    const n = Math.max(1, Math.round(L / pitch))\n    const dot = ink * f\n    // A stroke shorter than half a pitch is a dot: one leader, at its middle.\n    const spots = L < pitch / 2 ? [from + L / 2] : Array.from({ length: closed ? n : n + 1 }, (_, i) => from + (i * L) / n)\n    // A sharp corner (an arrowhead's point) always takes a leader of its own, the nearest one moved onto it, and\n    // stands a little closer to another stroke than the rest may: without its point an arrow is a line.\n    const points = new Set<number>()\n    if (spots.length > 2) for (const c of sharp(p)) {\n      const s = c * u\n      if (s <= from || s >= to) continue\n      let k = 0\n      spots.forEach((v, i) => { if (Math.abs(v - s) < Math.abs(spots[k] - s)) k = i })\n      if (k > 0 && k < spots.length - 1) { spots[k] = s; points.add(k) }\n    }\n    for (const [k, s] of spots.entries()) {\n      const q = at(p, s / u)\n      const box: Box = { x: q.x * u, y: q.y * u, r: 0, w: dot, h: dot, stroke }\n      // Another stroke's leader stands off by its box, or a pitch between centres (most of one at a sharp point), the\n      // spacing the strokes are ruled at, so a join reads as one ruling and an arrowhead keeps its point and barbs.\n      const apart = pitch * (points.has(k) ? 0.75 : 0.95)\n      const clear = (g: Box) => reach(g, box.x, box.y) >= pitch * 0.6 || Math.hypot(g.x - box.x, g.y - box.y) >= apart\n      // The word's letters (stroke -1) keep the leaders a little further off, so the word stands clear of its drawing.\n      if (boxes.every((g) => g.stroke === stroke || (g.stroke < 0 ? reach(g, box.x, box.y) >= Math.max(pitch * 0.6, g.h * 0.45) : clear(g)))) {\n        glyphs.push({ ch: LEADER, x: box.x, y: box.y, r: 0, k: f / ws, say: -1, ...(quiet ? { quiet } : null) })\n        boxes.push(box)\n      }\n    }\n  }\n\n  /** The text cut: each stroke ruled in leaders, ends included. The dots grow more slowly than the sign, so a\n   * large one is drawn finer, as a large icon's stroke is. The word's letters wait unseen at points spread along\n   * the whole drawing, and rise out of them when it's said. */\n  const ruleDots = (f = S <= 24 ? S * 0.66 : 15.84 * (S / 24) ** 0.7) => {\n    lines.forEach((path, stroke) => { const p = sample(path); leaders(p, 0, p.length * u, f, stroke) })\n    const dots = glyphs.slice()\n    letters.forEach((ch, j) => {\n      const d = dots[Math.floor(((j + 0.5) * dots.length) / letters.length)]\n      if (d) glyphs.push({ ch, x: d.x, y: d.y, r: 0, k: (S * 0.3) / ws, say: j, hush: true })\n    })\n  }\n\n  /** The word cut, set as a typographer would set it by hand: the word once, in ink, on the one stroke that carries\n   * it best, and the rest of the drawing ruled in pencil leaders that stop short of it. The best stroke is the one\n   * that takes the word largest and most nearly level: read from whichever end keeps it upright, slid along a curve\n   * to where it stands straightest, opened by at most 0.08em to reach a little further along a straight one, never\n   * turning more than 80° in all or kinking at a corner, never crowding its letters' tops on the inside of a bend.\n   * A drawing whose strokes are all too short to hold the word at a size a person reads (a chevron, a star's\n   * points) is drawn in leaders a little smaller, and the word set straight beneath it, as a road sign carries its\n   * legend; that wins too where it reads clearly larger than the word could on any stroke. */\n  const setWord = () => {\n    const pieces = lines.map(sample)\n    const base = m.advances(letters, rest, false)\n    const em = base.reduce((a, b) => a + b, 0)\n    const f0 = S * Math.max(0.15, Math.min(0.3, 0.3 - 0.075 * Math.log2(S / 32)))\n    const most = f0 * 1.5, least = Math.max(10, f0 * 0.7)\n    const meets = (i: number, [x, y]: [number, number]) => pieces.some((q, j) => j !== i && q.pts.some(([qx, qy]) => Math.hypot(qx - x, qy - y) < 0.75))\n    const top = (b: Box) => { const a = (b.r * Math.PI) / 180; return [b.x + (Math.sin(a) * b.h) / 2, b.y - (Math.cos(a) * b.h) / 2] }\n    const crowd = (p: Box, q: Box) => { const [px, py] = top(p), [qx, qy] = top(q); return Math.hypot(px - qx, py - qy) < 0.75 * Math.hypot(p.x - q.x, p.y - q.y) }\n    const turn = (r: number) => ((((r + 180) % 360) + 360) % 360) - 180\n    // Every stroke's points in pixels, a point a unit, for telling where the word would cover the drawing.\n    const others: [number, number, number][] = []\n    pieces.forEach((q, j) => { let last = -1; q.pts.forEach(([x, y], i) => { if (q.along[i] - last >= 1) { last = q.along[i]; others.push([x * u, y * u, j]) } }) })\n    type Take = { score: number; f: number; set: Glyph[]; put: Box[] }\n    const longest = Math.max(...pieces.map((p) => Math.max(...runs(p))))\n    let best: Take | null = null\n\n    /** The word laid along p from s, at size f with track added between letters; null where it can't read there. */\n    const place = (p: Sampled, s: number, f: number, track: number, stroke: number, straight: number[], bias: number): Take | null => {\n      const set: Glyph[] = [], put: Box[] = []\n      const from = s\n      let swing = 0, widest = 0, lean = 0\n      for (let i = 0; i < letters.length; i++) {\n        const a = base[i] * f\n        const q = at(p, (s + a / 2) / u)\n        if (Math.abs(q.r) > 95) return null\n        // A word of two letters has nothing round it to read by: turned on end, \"up\" reads \"dn\".\n        if (letters.length < 3 && Math.abs(q.r) > 30) return null\n        if (i) {\n          const d = turn(q.r - put[i - 1].r)\n          if (Math.abs(d) > 20) return null // a kink: the word would break at a corner\n          swing += d\n          widest = Math.max(widest, Math.abs(swing))\n        }\n        set.push({ ch: letters[i], x: q.x * u, y: q.y * u, r: q.r, k: f / ws, say: i })\n        put.push({ x: q.x * u, y: q.y * u, r: q.r, w: a * 0.86, h: f * HEIGHT, stroke: -1 })\n        lean += Math.abs(q.r)\n        s += a + track\n      }\n      if (widest > 120) return null\n      // Larger and more level is better, and a longer run a little better: an arrow says its word along its shaft.\n      const mid = ((from + s) / 2) / u\n      let k = 1\n      while (k < p.along.length - 1 && p.along[k] < mid) k++\n      const level = bias * f * (1 - (0.3 * lean) / letters.length / 90) * (Math.min(straight[k], pieces[stroke].length) / longest) ** 0.7\n      if (best && level <= best.score) return null // can't beat what's found: no need to look closer\n      if (put.some((b, i) => i && (touch(put[i - 1], b) || crowd(put[i - 1], b)))) return null\n      // Over another stroke the word would hide part of the drawing: allowed, but it costs.\n      let over = 0\n      const [bx0, bx1] = [Math.min(...put.map((b) => b.x)) - f, Math.max(...put.map((b) => b.x)) + f]\n      const [by0, by1] = [Math.min(...put.map((b) => b.y)) - f, Math.max(...put.map((b) => b.y)) + f]\n      for (const [x, y, j] of others) if (j !== stroke && x > bx0 && x < bx1 && y > by0 && y < by1 && put.some((b) => reach(b, x, y) < f * 0.1)) over++\n      return { score: level * (over ? Math.max(0.5, 1 - over * 0.02) : 1), f, set, put }\n    }\n\n    pieces.forEach((p0, stroke) => {\n      const [a0, a1] = [p0.pts[0], p0.pts[p0.pts.length - 1]]\n      const closed = Math.hypot(a0[0] - a1[0], a0[1] - a1[1]) < 0.01\n      const ends = [meets(stroke, a0), meets(stroke, a1)]\n      // Read from either end; where only one end meets the drawing, toward it a little rather: an arrow's word runs to its head.\n      for (const [p, start, end] of [[p0, ends[0], ends[1]], [backwards(p0), ends[1], ends[0]]] as const) {\n        const len = p.length * u\n        const path = closed ? twice(p) : p\n        const straight = runs(path)\n        for (let f = Math.min(most, (len * 0.96) / em); f >= least; f *= 0.93) {\n          const corner = f * 0.36\n          const lo = closed || !start ? 0 : corner\n          const hi = closed || !end ? len : len - corner\n          const run = em * f\n          if (!closed && run > hi - lo) continue\n          // Open a little toward the ends of a straight stroke, never past 0.08em.\n          const track = closed || letters.length < 2 ? 0 : Math.min(0.08 * f, (hi - lo - run) / (letters.length - 1) / 3)\n          const span = run + track * (letters.length - 1)\n          const room = closed ? len : hi - lo - span\n          const steps = closed ? 48 : Math.min(12, Math.ceil(room / (f * 0.25)))\n          let found = false\n          for (let i = 0; i <= steps; i++) {\n            const s = closed ? (i * len) / steps : lo + (steps ? (room * i) / steps : room / 2)\n            const take = place(path, s, f, track, stroke, straight, !start && end ? 1.03 : 1)\n            if (!take) continue\n            found = true\n            // On an open stroke the word sits best at its middle.\n            if (!closed && room > 0) take.score *= 1 - 0.08 * Math.abs((s - lo) / room - 0.5)\n            if (!best || take.score > best.score) best = take\n          }\n          if (found) break\n        }\n      }\n    })\n\n    const word = best as Take | null // set inside the loop's closures, which narrowing can't see\n    // The legend: the drawing smaller, the word straight beneath it at a size a person reads.\n    const fc = Math.min(f0 * 1.1, (S * 0.96) / em)\n    if (fc >= 9 && (!word || word.f < fc * 0.85 || fc * 0.7 > word.score)) {\n      const pts = pieces.flatMap((p) => p.pts)\n      const [x0, x1] = [Math.min(...pts.map((q) => q[0])), Math.max(...pts.map((q) => q[0]))]\n      const [y0, y1] = [Math.min(...pts.map((q) => q[1])), Math.max(...pts.map((q) => q[1]))]\n      const line = (fc * HEIGHT) / u, gap = (fc * 0.45) / u, edge = 1.5\n      const g = Math.min(1, (24 - 2 * edge) / Math.max(x1 - x0, 0.01), (24 - 2 * edge - line - gap) / Math.max(y1 - y0, 0.01))\n      const dy = (24 - (line + gap + (y1 - y0) * g)) / 2\n      const cx = (x0 + x1) / 2\n      const moved = pieces.map((p) => ({ ...p, pts: p.pts.map(([x, y]) => [12 + (x - cx) * g, dy + (y - y0) * g] as [number, number]), along: p.along.map((a) => a * g), length: p.length * g }))\n      let x = (S - em * fc) / 2\n      const y = (dy + (y1 - y0) * g + gap + line / 2) * u\n      const set: Glyph[] = [], put: Box[] = []\n      letters.forEach((ch, i) => {\n        const a = base[i] * fc\n        set.push({ ch, x: x + a / 2, y, r: 0, k: fc / ws, say: i })\n        put.push({ x: x + a / 2, y, r: 0, w: a * 0.86, h: fc * HEIGHT, stroke: -1 })\n        x += a\n      })\n      glyphs.push(...set)\n      boxes.push(...put)\n      moved.forEach((p, stroke) => leaders(p, 0, p.length * u, f0 * Math.sqrt(g), stroke, true))\n      return true\n    }\n    if (!word) return false\n    glyphs.push(...word.set)\n    boxes.push(...word.put)\n    pieces.forEach((p, stroke) => leaders(p, 0, p.length * u, f0, stroke, true))\n    return true\n  }\n\n  if (mode === \"dots\") ruleDots()\n  else if (mode === \"words\") {\n    // A word that can't be set at a size a person reads anywhere in the drawing: the drawing in dots.\n    if (!setWord()) {\n      mode = \"dots\"\n      ruleDots()\n    }\n  } else if (shape.fill) {\n    // The silhouette in rows, a little tighter than the type's own leading so it reads as one shape. The word runs\n    // on from one run to the next, as a paragraph does round a picture; each run is spread to its ends so the edge\n    // is the outline, but never so far it falls apart into letters.\n    const { step, rows } = shape.fill\n    const f = step * u * 1.3\n    const adv = m.advances(letters, rest, true).map((a) => a * f)\n    const run = (from: number, n: number) => { let s = 0; for (let i = 0; i < n; i++) s += adv[(from + i) % adv.length]; return s }\n    let from = 0\n    for (const [y, ...runs] of rows) {\n      for (let r = 0; r + 1 < runs.length; r += 2) {\n        const a = runs[r] * u, cw = (runs[r + 1] - runs[r]) * u\n        let n = 0\n        while (run(from, n + 1) <= cw * 1.06) n++\n        if (!n) {\n          if (cw < adv[from % adv.length] * 0.55) continue // too narrow for a letter: paper\n          n = 1\n        }\n        const spread = Math.min(cw / run(from, n), 1.35)\n        const inset = (cw - run(from, n) * spread) / 2\n        let s = 0\n        for (let i = 0; i < n; i++) {\n          const w = adv[(from + i) % adv.length]\n          glyphs.push({ ch: letters[(from + i) % letters.length], x: a + inset + (s + w / 2) * spread, y: y * u, r: 0, k: f / ws, say: -1 })\n          s += w\n        }\n        from += n\n      }\n    }\n  }\n\n  // The letters that say the word: the first whole word in reading order, else each letter's first appearance.\n  if (mode !== \"dots\") {\n    const first = glyphs.findIndex((_, i) => letters.every((ch, j) => glyphs[i + j]?.ch === ch))\n    letters.forEach((ch, j) => {\n      const g = first >= 0 ? glyphs[first + j] : glyphs.find((x) => x.ch === ch && x.say < 0)\n      if (g) g.say = j\n    })\n  }\n  // Where they stand when it's said: the word set straight across the middle of the square.\n  const wadv = sayAdv.map((a) => a * ws)\n  let x = (S - sayEm * ws) / 2\n  const word = wadv.map((a) => { const p = { x: r2(x + a / 2), y: r2(S / 2) }; x += a; return p })\n  for (const g of glyphs) {\n    g.x = r2(g.x)\n    g.y = r2(g.y)\n    g.k = Math.round(g.k * 1000) / 1000\n    // A turn the short way round, so a letter rights itself rather than spinning.\n    g.r = r2(((((g.r + 180) % 360) + 360) % 360) - 180)\n  }\n  return { glyphs, ws: r2(ws), rest, said, word }\n}\n\n/** The side in pixels a size gives on the server: pixels, px or rem. Anything else is measured in the browser. */\nfunction side(size: number | string | undefined) {\n  if (size == null) return 24\n  if (typeof size === \"number\") return size\n  const n = parseFloat(size)\n  if (size.endsWith(\"rem\")) return n * 16\n  if (size.endsWith(\"px\")) return n\n  return 24\n}\n\n/**\n * A sign: an icon made of its own word. The word is set along the strokes of the drawing (words), the strokes are\n * ruled in middle-dot leaders (dots), or the word fills the silhouette row by row (fill), so type stays the only\n * ornament and no second visual language arrives. It lays out on the server from measured advances; pretext sets\n * it again in the browser with the face's kerning. Pointing at it, at the control it sits in, or focusing that\n * control, sets the word: the letters leave the drawing in reading order, one arpeggio apart, and stand up as the\n * word they always were; the echoes go quiet. Leaving winds them back. Under reduced motion the word and the\n * drawing change places without travel.\n */\nfunction Sign({ shape, variant = \"words\", face = \"roman\", size, label, className, style, ref: forwardedRef, ...props }: SignProps) {\n  const ref = React.useRef<HTMLSpanElement>(null)\n  const composedRef = useComposedRefs(ref, forwardedRef)\n  const italic = face === \"italic\"\n  const [laid, setLaid] = React.useState<Laid>(() => layoutSign(shape, variant, side(size), italic, tableMetrics(italic)))\n\n  React.useEffect(() => {\n    const el = ref.current\n    if (!el) return\n    let cancelled = false\n    const off: (() => void)[] = []\n    const table = tableMetrics(italic)\n\n    ;(async () => {\n      let lib: typeof import(\"@chenglou/pretext\") | null = null\n      try {\n        lib = await import(\"@chenglou/pretext\")\n      } catch {\n        // The measured table stands in: no kerning, otherwise the same.\n      }\n      const widths = new Map<string, number>()\n\n      async function lay() {\n        if (cancelled || !el!.isConnected) return\n        const S = el!.clientWidth\n        if (!S) return\n        const css = getComputedStyle(el!)\n        const turned = shape.mirror && css.direction === \"rtl\" ? mirrored(shape) : shape\n        const font = (w: number) => `${italic ? \"italic \" : \"\"}${w} ${REF}px ${css.fontFamily}`\n        let metrics = table\n        if (lib) {\n          const t = Math.max(0, Math.min(3, Math.log2(S / 16)))\n          const weights = [Math.round(800 - 100 * t), italic ? 500 : 560]\n          try {\n            await Promise.all(weights.map((w) => document.fonts.load(font(w), shape.word + LEADER)))\n          } catch {\n            // A face that won't load is measured as it falls back.\n          }\n          if (cancelled || !el!.isConnected) return\n          const pretext = lib\n          const width = (s: string, w: number) => {\n            const key = `${font(w)}|${s}`\n            let v = widths.get(key)\n            if (v == null) widths.set(key, (v = s ? pretext.measureNaturalWidth(pretext.prepareWithSegments(s, font(w))) / REF : 0))\n            return v\n          }\n          let ink = Infinity\n          const c = document.createElement(\"canvas\").getContext(\"2d\")\n          metrics = {\n            // Each letter's advance, kerning included: the width up to and with it, less the width before it.\n            advances: (letters, w, wrap) => {\n              let before = 0\n              const out = letters.map((_, i) => { const upto = width(letters.slice(0, i + 1).join(\"\"), w); const a = upto - before; before = upto; return a })\n              const word = letters.join(\"\")\n              if (wrap && out.length) out[out.length - 1] += width(word + word, w) - 2 * width(word, w)\n              return out\n            },\n            leader: (w) => {\n              if (c && ink === Infinity) {\n                c.font = font(w)\n                const m = c.measureText(LEADER)\n                ink = (m.actualBoundingBoxLeft + m.actualBoundingBoxRight) / REF\n              }\n              return { advance: width(LEADER, w), ink: ink === Infinity ? table.leader(w).ink : ink }\n            },\n          }\n        }\n        if (!cancelled) setLaid(layoutSign(turned, variant, S, italic, metrics))\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      const restyled = new MutationObserver(() => lay())\n      restyled.observe(document.documentElement, { attributeFilter: [...FACE, \"dir\"] })\n      off.push(() => { resized.disconnect(); restyled.disconnect() })\n    })()\n\n    return () => {\n      cancelled = true\n      off.forEach((f) => f())\n    }\n  }, [shape, variant, italic])\n\n  const hidden = label === \"\"\n  return (\n    <span\n      ref={composedRef}\n      data-slot=\"sign\"\n      data-variant={variant}\n      data-face={italic ? \"italic\" : undefined}\n      role={hidden ? undefined : \"img\"}\n      aria-label={hidden ? undefined : (label ?? shape.word)}\n      aria-hidden={hidden || undefined}\n      className={cn(\"ot-sign\", className)}\n      style={{ ...(size != null ? { \"--ot-sign-size\": typeof size === \"number\" ? `${size}px` : size } : null), \"--ws\": `${laid.ws}px`, \"--rest\": laid.rest, \"--said\": laid.said, ...style } as React.CSSProperties}\n      {...props}\n    >\n      {laid.glyphs.map((g, i) => (\n        <span\n          key={i}\n          className=\"ot-sign-glyph\"\n          data-say={g.say >= 0 ? \"\" : undefined}\n          data-hush={g.hush || undefined}\n          data-quiet={g.quiet || undefined}\n          style={{ \"--x\": `${g.x}px`, \"--y\": `${g.y}px`, \"--r\": `${g.r}deg`, \"--k\": g.k, ...(g.say >= 0 ? { \"--n\": g.say, \"--m\": laid.word.length - 1 - g.say, \"--wx\": `${laid.word[g.say].x}px`, \"--wy\": `${laid.word[g.say].y}px` } : null) } as React.CSSProperties}\n        >\n          {g.ch}\n        </span>\n      ))}\n    </span>\n  )\n}\n\nexport { Sign, layoutSign, mirrored, tableMetrics, type Move, type Path, type SignProps, type SignShape, type SignVariant }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/0nlytype/styles/sign.css",
      "content": "@layer components {\n  /* ── ot-sign ── an icon made of its own word, after the ampersand (the word \"et\" worn down into a sign) and\n     Apollinaire's calligrams. Pretext rules the drawing's strokes in middle-dot leaders (dots), lays the word along\n     them (words), or sets it in rows that fill the drawing's silhouette (fill). Each letter is set at the size it is said at and scaled down into the drawing, so saying it is one\n     transform: it travels to its place in the word, turns upright and comes to full size. */\n  .ot-sign {\n    --ot-sign-size: 1.5rem;\n    position: relative; display: inline-block; flex: none; vertical-align: middle;\n    width: var(--ot-sign-size); height: var(--ot-sign-size);\n    font-family: var(--ot-voice); font-style: normal; color: inherit;\n  }\n  .ot-sign[data-face=\"italic\"] { font-family: var(--ot-expression); font-style: italic; }\n  .ot-sign-glyph {\n    position: absolute; left: 0; top: 0; pointer-events: none; transform-origin: 0 0; /* placed in the square's own coordinates, so the same either way round */\n    font-size: var(--ws); line-height: 1; white-space: pre; font-weight: var(--rest);\n    /* The letter's middle on the stroke: the em box's centre sits a little above the x-height's. */\n    transform: translate(var(--x), var(--y)) rotate(var(--r)) scale(var(--k)) translate(-50%, -54%);\n    transition: transform var(--ot-allegro) var(--ot-breath), font-weight var(--ot-allegro) var(--ot-breath), opacity var(--ot-allegro) linear, color var(--ot-allegro) linear;\n    /* Going home, the last letter of the word leaves first; the echoes come back once the word has gone. */\n    transition-delay: calc(var(--m, 0) * var(--ot-arpeggio));\n  }\n  .ot-sign-glyph:not([data-say]) { transition-delay: calc(var(--ot-arpeggio) * 2); }\n  /* In the text cut the word's letters wait unseen at the leaders, and rise out of them when it's said. */\n  .ot-sign-glyph[data-hush] { opacity: 0; }\n  /* In the word cut the drawing round the word is ruled in pencil, so the word is the one thing in ink. */\n  .ot-sign-glyph[data-quiet] { color: var(--ot-pencil); }\n  .ot-sign[data-face=\"italic\"] .ot-sign-glyph { font-variation-settings: var(--ot-expression-text-variation, normal); }\n\n  /* Said: pointing at the sign, or at the control it sits in, or focusing that control, sets the word. The echoes\n     sketch out in pencil and go quiet. data-force=\"hover\" pins it for a states row. */\n  .ot-sign:is([data-force~=\"hover\"], [data-force~=\"hover\"] *, :is(a, button, summary, [role=\"button\"], [tabindex]):focus-visible *) .ot-sign-glyph[data-say] {\n    transform: translate(var(--wx), var(--wy)) rotate(0deg) scale(1) translate(-50%, -54%);\n    font-weight: var(--said); opacity: 1;\n    transition-duration: var(--ot-moderato); transition-delay: calc(var(--n, 0) * var(--ot-arpeggio));\n  }\n  .ot-sign:is([data-force~=\"hover\"], [data-force~=\"hover\"] *, :is(a, button, summary, [role=\"button\"], [tabindex]):focus-visible *) .ot-sign-glyph:not([data-say]) {\n    opacity: 0; color: var(--ot-pencil); transition-delay: 0s;\n  }\n  @media (hover: hover) {\n    .ot-sign:is(:hover, :is(a, button:not(:disabled), summary, label, [role=\"button\"]):hover *) .ot-sign-glyph[data-say] {\n      transform: translate(var(--wx), var(--wy)) rotate(0deg) scale(1) translate(-50%, -54%);\n      font-weight: var(--said); opacity: 1;\n      transition-duration: var(--ot-moderato); transition-delay: calc(var(--n, 0) * var(--ot-arpeggio));\n    }\n    .ot-sign:is(:hover, :is(a, button:not(:disabled), summary, label, [role=\"button\"]):hover *) .ot-sign-glyph:not([data-say]) {\n      opacity: 0; color: var(--ot-pencil); transition-delay: 0s;\n    }\n  }\n  @media (forced-colors: active) { .ot-sign-glyph { color: CanvasText; } .ot-sign-glyph[data-quiet] { color: GrayText; } }\n}\n",
      "type": "registry:file",
      "target": "styles/0nlytype/sign.css"
    },
    {
      "path": "ICON-NOTICES.md",
      "content": "# Icon notices\n\n0nlyType signs derive their geometry from the Lucide project (https://lucide.dev). The geometry comes from `lucide-static` and is licensed under the ISC License.\n\n```\nISC License\n\nCopyright (c) 2026 Lucide Icons and Contributors\n\nPermission to use, copy, modify, and/or distribute this software for any\npurpose with or without fee is hereby granted, provided that the above\ncopyright notice and this permission notice appear in all copies.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\nWITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\nMERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\nANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\nWHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\nACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF\nOR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n---\n\nThe following Lucide icons are derived from the Feather project:\n\nairplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out\n\nThe MIT License (MIT) (for the icons listed above)\n\nCopyright (c) 2013-present Cole Bemis\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n```\n",
      "type": "registry:file",
      "target": "styles/0nlytype/ICON-NOTICES.md"
    }
  ],
  "css": {
    "@import \"../styles/0nlytype/sign.css\"": {}
  },
  "meta": {
    "movement": "II",
    "underneath": "hook",
    "contract": "ot-sign"
  },
  "type": "registry:ui"
}
