{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "not-found",
  "type": "registry:component",
  "title": "Not Found",
  "description": "An error code drawn in pixels that come apart near the pointer, with a title and a way out.",
  "dependencies": [
    "@base-ui/react",
    "tailwind-variants"
  ],
  "registryDependencies": [],
  "cssVars": {
    "light": {
      "success": "oklch(0.696 0.17 162.48)",
      "warning": "oklch(0.769 0.188 70.08)",
      "info": "oklch(0.623 0.214 259.815)"
    },
    "dark": {
      "success": "oklch(0.696 0.17 162.48)",
      "warning": "oklch(0.769 0.188 70.08)",
      "info": "oklch(0.623 0.214 259.815)"
    }
  },
  "files": [
    {
      "path": "components/panelui/not-found/index.tsx",
      "type": "registry:component",
      "target": "@components/panelui/not-found/index.tsx",
      "content": "'use client';\n\n/**\n * NotFound — an error code drawn in pixels that come apart when the pointer\n * reaches them, with a title, a line of help and a way out underneath.\n *\n * ```tsx\n * <NotFound>\n *   <NotFound.Code code=\"404\" effect=\"scatter\" />\n *   <NotFound.Title>Nothing lives here</NotFound.Title>\n *   <NotFound.Description>The address may be mistyped, or the page has moved.</NotFound.Description>\n *   <NotFound.Actions>…</NotFound.Actions>\n * </NotFound>\n * ```\n *\n * ## A grid of cells, not a font\n *\n * Each digit is a small bitmap laid on a grid of cells, and every cell is\n * drawn as a dot or a rounded square of its own size and strength. The sizes\n * and strengths are fixed per cell from a seed, so the code has the uneven,\n * hand-placed texture of pixel art and still looks the same on every render.\n * Nothing depends on a typeface loading.\n *\n * ## One digit reacts at a time\n *\n * The digit nearest the pointer is the one that reacts, and it is put back\n * when the pointer moves away. `effect` chooses how: its cells fly apart\n * (`scatter`), drop out one by one (`dissolve`), or travel into a small icon\n * and back (`scramble`). On a touch screen a tap does the same, and the digit\n * is put back a moment after the finger lifts.\n *\n * ## Two colours, mixed by light\n *\n * Every cell is either the text colour, faint, or the accent. Which one it is\n * depends on how much light falls on it: the highlighted digit is always\n * partly lit, a band of light goes slowly round the whole code, and the\n * pointer lights the cells near it and leaves them fading as it moves on. Each\n * cell turns accent at its own level of light, so a shape in half light is\n * dithered between the two colours, the way pixel art is shaded.\n *\n * ## What it costs\n *\n * The cells are drawn on a canvas, a few hundred of them in about sixteen\n * fills. The band asks for 30 frames a second while the code is on screen and\n * none once it is scrolled away; with `ambient={false}` a frame is only drawn\n * while something is moving. The canvas reads its colours from the theme each\n * time it draws, so a change of theme needs nothing from the page.\n */\nimport { useEffect, useRef, type ComponentProps, type CSSProperties } from 'react';\nimport { tv } from 'tailwind-variants';\nimport { GLYPH_COLUMNS, GLYPH_ROWS, ICONS, digitGlyph, type Glyph } from './glyphs';\nimport { mergeProps } from '@base-ui/react/merge-props';\nimport { useRender } from '@base-ui/react/use-render';\nimport type { ElementType as RenderElementType } from 'react';\n\n/** Draws a part as another element, or composes it with a component: `render={<a href=\"/\" />}`. */\ntype RenderProp<Tag extends RenderElementType> = { render?: useRender.ComponentProps<Tag>['render'] };\n\n/** How the digit near the pointer reacts. */\nexport type NotFoundEffect = 'scatter' | 'dissolve' | 'scramble';\n\n/** Which of the theme's roles the highlighted digit is drawn in. */\nexport type NotFoundColor = 'primary' | 'info' | 'success' | 'warning' | 'destructive';\n\nconst notFoundVariants = tv({\n  slots: {\n    // `overflow-x-clip` keeps flying cells from widening the page on a phone.\n    // It clips one axis only, so nothing above or below is cut off.\n    root: 'relative isolate flex w-full flex-col items-center gap-3 overflow-x-clip px-4 text-center',\n    // A faint grid with a small cross where its lines meet, fading out towards\n    // the edges. Drawn in the text colour at a few percent, so it suits either theme.\n    backdrop: 'pointer-events-none absolute inset-0 -z-10 bg-foreground/15',\n    code: 'relative mb-6 w-full max-w-xl select-none',\n    // The canvas is larger than the box the code takes in the layout, so\n    // scattered cells have somewhere to go. It takes no pointer events: the\n    // page under it stays usable while cells are flying over it.\n    canvas: 'pointer-events-none absolute text-foreground [outline-color:var(--pu-color)]',\n    title: 'text-balance text-2xl font-semibold tracking-tight text-foreground sm:text-3xl',\n    description: 'max-w-md text-balance text-base text-muted-foreground',\n    actions: 'mt-4 flex flex-wrap items-center justify-center gap-2',\n  },\n  variants: {\n    backdrop: {\n      true: { root: 'py-12' },\n    },\n    color: {\n      primary: { code: '[--pu-color:var(--primary)]' },\n      info: { code: '[--pu-color:var(--info)]' },\n      success: { code: '[--pu-color:var(--success)]' },\n      warning: { code: '[--pu-color:var(--warning)]' },\n      destructive: { code: '[--pu-color:var(--destructive)]' },\n    },\n  },\n  defaultVariants: { color: 'info' },\n});\n\n/* -------------------------------------------------------------------------- */\n/* Layout                                                                      */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Empty cells between two digits: two of a digit's own pixels. Any closer and\n * the outlines of neighbouring digits read as one pattern.\n */\nconst GAP = 6;\n/** Room round the code, in cells, for scattered cells to fly into. */\nconst PAD_X = 10;\nconst PAD_Y = 8;\n\ninterface Layout {\n  /** Cells across the code itself. */\n  columns: number;\n  /** The first column of each character. */\n  lefts: number[];\n}\n\nfunction layoutOf(code: string): Layout {\n  const lefts: number[] = [];\n  let x = 0;\n  for (let i = 0; i < code.length; i += 1) {\n    if (i > 0) x += GAP;\n    lefts.push(x);\n    x += GLYPH_COLUMNS;\n  }\n  return { columns: Math.max(x, 1), lefts };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Engine                                                                      */\n/* -------------------------------------------------------------------------- */\n\n/** A small fast hash, so a cell's look is the same on every render. */\nfunction seeded(n: number) {\n  let t = (n * 0x9e3779b9) >>> 0;\n  t = Math.imul(t ^ (t >>> 16), 0x85ebca6b) >>> 0;\n  t = Math.imul(t ^ (t >>> 13), 0xc2b2ae35) >>> 0;\n  return ((t ^ (t >>> 16)) >>> 0) / 4294967296;\n}\n\nconst clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max);\nconst easeInOut = (t: number) => (t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2);\n\n/** How strong an unlit cell is drawn: three strengths, picked per cell. */\nconst PLAIN_TONES = [0.22, 0.34, 0.48];\n/** How lit the highlighted digit is with nothing else lighting it. */\nconst HIGHLIGHT_LIGHT = 0.72;\n/** How far the pointer's light reaches, in cells. */\nconst HEAT_RADIUS = 5;\n/** How fast a cell lights under the pointer, and how fast it fades after, per second. */\nconst HEAT_RISE = 22;\nconst HEAT_FALL = 2.6;\n/** How fast the band of light goes round the code, in radians a second. */\nconst BAND_SPEED = 0.55;\n/** How wide the band is, in radians either side of its middle. */\nconst BAND_WIDTH = 0.7;\n/** Steps of opacity the cells are batched into. Each is one fill per frame. */\nconst ALPHA_STEPS = 12;\n\n/** Cells within this distance of a digit, in cells, make it react. */\nconst APPROACH = 2.5;\n/** And it is put back once the pointer is this far away. */\nconst RELEASE = 4.5;\n/** How long a tapped digit stays apart after the finger lifts, in ms. */\nconst TOUCH_HOLD = 700;\n\ninterface Particle {\n  /** Home, in cells from the canvas's top left. */\n  hx: number;\n  hy: number;\n  x: number;\n  y: number;\n  vx: number;\n  vy: number;\n  /** Where Scatter sends it, relative to home. */\n  sx: number;\n  sy: number;\n  /** A dot or a rounded square. */\n  square: boolean;\n  /** Its size, as a share of a cell. */\n  size: number;\n  tone: number;\n  seed: number;\n  /** Lit above this much light. Different per cell, so light dithers across a shape. */\n  pick: number;\n  /** Light left by the pointer, 0 to 1, fading after it moves on. */\n  heat: number;\n  /** Dissolve takes it out once the digit's progress passes this. */\n  threshold: number;\n  /** Scramble: where it leaves from and goes to, and how it fades on the way. */\n  fx: number;\n  fy: number;\n  tx: number;\n  ty: number;\n  /** 1 fades in on the way, -1 fades out, 0 stays. */\n  fade: number;\n}\n\ninterface Digit {\n  left: number;\n  accent: boolean;\n  /** The cells at home. */\n  cells: Particle[];\n  /** What is drawn: the cells, or Scramble's travelling copy of them. */\n  particles: Particle[];\n  active: boolean;\n  /** Dissolve and Scramble's progress, 0 at rest and 1 fully apart. */\n  amount: number;\n  /** The icon Scramble turns it into next. */\n  icon: number;\n  /** Where Scramble's cells gather on the way. */\n  mx: number;\n  my: number;\n}\n\ninterface Engine {\n  setEffect(effect: NotFoundEffect): void;\n  destroy(): void;\n}\n\ninterface EngineOptions {\n  canvas: HTMLCanvasElement;\n  code: string;\n  fill: 'outline' | 'solid';\n  highlight: number | null;\n  effect: NotFoundEffect;\n  ambient: boolean;\n}\n\nfunction makeCell(hx: number, hy: number, salt: number): Particle {\n  const seed = seeded(salt);\n  const pick = seeded(salt + 7919);\n  return {\n    hx,\n    hy,\n    x: hx,\n    y: hy,\n    vx: 0,\n    vy: 0,\n    sx: 0,\n    sy: 0,\n    // Mostly dots, with a square here and there.\n    square: pick < 0.28,\n    size: 0.56 + seeded(salt + 104729) * 0.34,\n    tone: Math.floor(seeded(salt + 1299709) * 3),\n    seed,\n    pick: seeded(salt + 15485863),\n    heat: 0,\n    threshold: seed,\n    fx: hx,\n    fy: hy,\n    tx: hx,\n    ty: hy,\n    fade: 0,\n  };\n}\n\nfunction cellsOf(glyph: Glyph, left: number, salt: number): Particle[] {\n  const cells: Particle[] = [];\n  glyph.forEach((row, y) =>\n    row.forEach((on, x) => {\n      if (on) cells.push(makeCell(PAD_X + left + x, PAD_Y + y, salt * 4099 + y * 97 + x));\n    })\n  );\n  return cells;\n}\n\n/** Cells sorted by their angle round a point, so two shapes can be paired off in order. */\nfunction byAngle<T extends { x: number; y: number }>(cells: T[], cx: number, cy: number): T[] {\n  return [...cells].sort((a, b) => Math.atan2(a.y - cy, a.x - cx) - Math.atan2(b.y - cy, b.x - cx));\n}\n\nfunction createEngine({ canvas, code, fill, highlight, effect: initialEffect, ambient }: EngineOptions): Engine {\n  const context = canvas.getContext('2d');\n  const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');\n  const layout = layoutOf(code);\n  const columns = layout.columns + PAD_X * 2;\n\n  let effect = initialEffect;\n  let pitch = 0;\n  let width = 0;\n  let height = 0;\n  let frame = 0;\n  let last = 0;\n  let time = 0;\n  let destroyed = false;\n  let visible = true;\n  /** The pointer, in cells, or `null` when it is not over the page. */\n  let pointer: { x: number; y: number } | null = null;\n  let touchTimer = 0;\n  /** 1 while the code is first being drawn in, falling to 0. */\n  let intro = reducedMotion.matches ? 0 : 1;\n  /** Whether any cell is still lit by the pointer, as of the last frame drawn. */\n  let warm = false;\n  let lastDraw = 0;\n  /** The middle of the code, and half its size, in cells. The band goes round this. */\n  const middle = { x: PAD_X + layout.columns / 2, y: PAD_Y + GLYPH_ROWS / 2 };\n  const half = { x: layout.columns / 2, y: GLYPH_ROWS / 2 };\n\n  const digits: Digit[] = [...code].map((character, index) => {\n    const glyph = digitGlyph(character, fill);\n    const cells = glyph ? cellsOf(glyph, layout.lefts[index] ?? 0, index + 1) : [];\n    return {\n      left: layout.lefts[index] ?? 0,\n      accent: index === highlight,\n      cells,\n      particles: cells,\n      active: false,\n      amount: 0,\n      icon: index % ICONS.length,\n      mx: 0,\n      my: 0,\n    };\n  });\n  // The highlighted digit is drawn last, so its debris flies over the others'.\n  const order = digits.map((_, i) => i).sort((a, b) => Number(digits[a]!.accent) - Number(digits[b]!.accent));\n\n  /* -------------------------- starting each effect -------------------------- */\n\n  const centreOf = (digit: Digit) => ({\n    x: PAD_X + digit.left + GLYPH_COLUMNS / 2,\n    y: PAD_Y + GLYPH_ROWS / 2,\n  });\n\n  const startScatter = (digit: Digit) => {\n    const from = pointer ?? centreOf(digit);\n    for (const cell of digit.cells) {\n      // Away from the pointer, spread a little either side, and further for\n      // the cells that were nearest it.\n      const away = Math.atan2(cell.hy - from.y, cell.hx - from.x);\n      const angle = away + (cell.seed - 0.5) * 1.6;\n      const near = 1 - clamp(Math.hypot(cell.hx - from.x, cell.hy - from.y) / 16, 0, 1);\n      const distance = 3 + seeded(cell.seed * 1e6) * 7 + near * 6;\n      cell.sx = Math.cos(angle) * distance;\n      cell.sy = Math.sin(angle) * distance;\n      // A kick in the same direction, so they burst rather than drift.\n      cell.vx += Math.cos(angle) * distance * 5;\n      cell.vy += Math.sin(angle) * distance * 5;\n    }\n  };\n\n  const startDissolve = (digit: Digit) => {\n    const from = pointer ?? centreOf(digit);\n    // Nearest the pointer goes first, with enough noise that it reads as\n    // pixels dropping out and not as a wipe.\n    for (const cell of digit.cells) {\n      const distance = clamp(Math.hypot(cell.hx - from.x, cell.hy - from.y) / 22, 0, 1);\n      cell.threshold = 0.62 * distance + 0.38 * cell.seed;\n    }\n  };\n\n  const startScramble = (digit: Digit) => {\n    const centre = centreOf(digit);\n    const from = byAngle(\n      digit.cells.map((cell) => ({ x: cell.hx, y: cell.hy, cell })),\n      centre.x,\n      centre.y\n    );\n    const icon = ICONS[digit.icon % ICONS.length]!;\n    const to = byAngle(\n      cellsOf(icon, digit.left, 0).map((cell) => ({ x: cell.hx, y: cell.hy })),\n      centre.x,\n      centre.y\n    );\n    const count = Math.max(from.length, to.length);\n    const particles: Particle[] = [];\n    const usedFrom = new Set<number>();\n    const usedTo = new Set<number>();\n    for (let i = 0; i < count; i += 1) {\n      const a = Math.floor((i * from.length) / count);\n      const b = Math.floor((i * to.length) / count);\n      const source = from[a];\n      const target = to[b];\n      if (!source || !target) continue;\n      const particle = { ...source.cell };\n      particle.fx = source.x;\n      particle.fy = source.y;\n      particle.tx = target.x;\n      particle.ty = target.y;\n      // A cell that already left, or a target already reached, is a second\n      // copy: it fades in or out so the count can change without a pile-up.\n      particle.fade = usedFrom.has(a) ? 1 : usedTo.has(b) ? -1 : 0;\n      usedFrom.add(a);\n      usedTo.add(b);\n      particles.push(particle);\n    }\n    digit.particles = particles;\n    // They gather near the pointer on the way, as if it had picked them up.\n    const at = pointer ?? centre;\n    digit.mx = at.x + (seeded(digit.icon + 31) - 0.5) * 3;\n    digit.my = at.y + (seeded(digit.icon + 37) - 0.5) * 3;\n  };\n\n  const activate = (index: number) => {\n    digits.forEach((digit, i) => {\n      if (i !== index && digit.active) digit.active = false;\n    });\n    const digit = digits[index];\n    if (!digit || digit.active) return;\n    digit.active = true;\n    if (effect === 'scatter') startScatter(digit);\n    else if (effect === 'dissolve') startDissolve(digit);\n    // Scramble only picks a new icon when the digit was whole. Caught on the\n    // way back, it turns round and heads for the same one.\n    else if (digit.amount === 0) startScramble(digit);\n    invalidate();\n  };\n\n  const releaseAll = () => {\n    let changed = false;\n    for (const digit of digits) {\n      if (digit.active) {\n        digit.active = false;\n        changed = true;\n      }\n    }\n    if (changed) invalidate();\n  };\n\n  /* --------------------------------- motion --------------------------------- */\n\n  /** Steps every digit on by `dt` seconds. True while anything is still moving. */\n  const step = (dt: number) => {\n    let moving = false;\n    const still = reducedMotion.matches;\n\n    if (intro > 0) {\n      intro = still ? 0 : Math.max(0, intro - dt / 0.9);\n      moving ||= intro > 0;\n    }\n    moving ||= warm;\n\n    for (const digit of digits) {\n      if (effect === 'scatter') {\n        if (still) {\n          digit.amount = digit.active ? 1 : 0;\n          for (const cell of digit.cells) {\n            cell.x = cell.hx;\n            cell.y = cell.hy;\n            cell.vx = 0;\n            cell.vy = 0;\n          }\n          continue;\n        }\n        // Stiffer on the way home than on the way out: the burst should feel\n        // loose, the return should snap shut.\n        const stiffness = digit.active ? 60 : 150;\n        const damping = 2 * Math.sqrt(stiffness) * (digit.active ? 0.7 : 0.92);\n        for (const cell of digit.cells) {\n          let tx = cell.hx;\n          let ty = cell.hy;\n          if (digit.active) {\n            // Scattered cells keep drifting, so the debris is not a still picture.\n            tx += cell.sx + Math.sin(time * 1.4 + cell.seed * 40) * 0.5;\n            ty += cell.sy + Math.cos(time * 1.2 + cell.seed * 70) * 0.5;\n          }\n          // Two half steps keep a stiff spring from overshooting at 30fps.\n          for (let s = 0; s < 2; s += 1) {\n            const h = dt / 2;\n            cell.vx += ((tx - cell.x) * stiffness - cell.vx * damping) * h;\n            cell.vy += ((ty - cell.y) * stiffness - cell.vy * damping) * h;\n            cell.x += cell.vx * h;\n            cell.y += cell.vy * h;\n          }\n          if (\n            !digit.active &&\n            Math.abs(cell.x - tx) < 0.004 &&\n            Math.abs(cell.y - ty) < 0.004 &&\n            Math.abs(cell.vx) + Math.abs(cell.vy) < 0.02\n          ) {\n            cell.x = tx;\n            cell.y = ty;\n            cell.vx = 0;\n            cell.vy = 0;\n          } else {\n            moving = true;\n          }\n        }\n        continue;\n      }\n\n      // Dissolve and Scramble run on one number per digit.\n      const target = digit.active ? 1 : 0;\n      if (digit.amount !== target) {\n        if (still) digit.amount = target;\n        else {\n          const duration = effect === 'scramble' ? 0.7 : digit.active ? 0.45 : 0.6;\n          digit.amount =\n            target > digit.amount\n              ? Math.min(target, digit.amount + dt / duration)\n              : Math.max(target, digit.amount - dt / duration);\n        }\n        moving ||= digit.amount !== target;\n      }\n      if (effect === 'scramble' && digit.amount === 0 && !digit.active && digit.particles !== digit.cells) {\n        // Home again. The next scramble goes to the next icon.\n        digit.particles = digit.cells;\n        digit.icon = (digit.icon + 1) % ICONS.length;\n      }\n    }\n    return moving;\n  };\n\n  /* --------------------------------- drawing -------------------------------- */\n\n  const draw = (dt = 0) => {\n    if (!context || width === 0) return;\n    const ratio = window.devicePixelRatio || 1;\n    const style = getComputedStyle(canvas);\n    const plain = style.color;\n    const accent = style.outlineColor || plain;\n    const still = reducedMotion.matches;\n\n    context.setTransform(ratio, 0, 0, ratio, 0, 0);\n    context.clearRect(0, 0, width, height);\n\n    // One path per colour, opacity step and shape. A few hundred cells come\n    // out as a handful of fills.\n    const paths = new Map<string, Path2D>();\n    const add = (isAccent: boolean, alpha: number, square: boolean, x: number, y: number, r: number) => {\n      if (alpha <= 0.02 || r <= 0.2) return;\n      const level = clamp(Math.round(alpha * ALPHA_STEPS), 1, ALPHA_STEPS);\n      const key = `${isAccent ? 1 : 0}${square ? 1 : 0}${level}`;\n      let path = paths.get(key);\n      if (!path) {\n        path = new Path2D();\n        paths.set(key, path);\n      }\n      if (square) {\n        if (typeof path.roundRect === 'function') path.roundRect(x - r, y - r, r * 2, r * 2, r * 0.35);\n        else path.rect(x - r, y - r, r * 2, r * 2);\n      } else {\n        path.moveTo(x + r, y);\n        path.arc(x, y, r, 0, Math.PI * 2);\n      }\n    };\n\n    // The band of light: where it is now, or held on the left without motion.\n    const band = still || !ambient ? Math.PI : time * BAND_SPEED;\n    let anyWarm = false;\n\n    for (const index of order) {\n      const digit = digits[index]!;\n      const amount = digit.amount;\n      // Stepped, so dissolving pixels go in visible ticks, the way pixel art moves.\n      const stepped = Math.floor(amount * 24) / 24;\n      const scrambleAt = easeInOut(amount);\n\n      for (const cell of digit.particles) {\n        let x = cell.x;\n        let y = cell.y;\n        let alpha = 1;\n        let size = cell.size;\n        let isAccent = false;\n\n        // Drawn in on first sight, in the same pixel-by-pixel way.\n        if (intro > 0 && cell.seed * 0.7 + index * 0.1 < intro) continue;\n\n        if (still && digit.active && effect !== 'scramble') {\n          // No movement asked for: the digit only dims.\n          alpha *= 0.25;\n        } else if (effect === 'scatter') {\n          const flown = clamp(Math.hypot(x - cell.hx, y - cell.hy) / 9, 0, 1);\n          size *= 1 - flown * 0.35;\n          alpha *= 1 - flown * 0.3;\n        } else if (effect === 'dissolve') {\n          if (cell.threshold < stepped) continue;\n          // The cells about to go, or just back, flash in the accent first.\n          if (cell.threshold - stepped < 0.07 && stepped > 0) {\n            isAccent = true;\n            size *= 1.2;\n            alpha = 1;\n          }\n        } else if (effect === 'scramble' && digit.particles !== digit.cells) {\n          // Each cell leaves a little after the last, through the gathering point.\n          const delay = cell.seed * 0.3;\n          const t = still ? amount : easeInOut(clamp((amount - delay) / 0.7, 0, 1));\n          const u = 1 - t;\n          x = u * u * cell.fx + 2 * u * t * digit.mx + t * t * cell.tx;\n          y = u * u * cell.fy + 2 * u * t * digit.my + t * t * cell.ty;\n          // Scattered a little mid-flight, so the crowd reads as noise.\n          const noise = Math.sin(Math.PI * t) * (still ? 0 : 1.4);\n          x += (seeded(cell.seed * 1e5) - 0.5) * noise;\n          y += (seeded(cell.seed * 2e5) - 0.5) * noise;\n          if (cell.fade === 1) alpha *= scrambleAt;\n          if (cell.fade === -1) alpha *= 1 - scrambleAt;\n        }\n\n        /*\n         * Light. Three things light a cell: being in the highlighted digit,\n         * the band going slowly round the code, and the pointer having been\n         * near. Each cell turns accent at its own level of light, so a shape\n         * half in the light is dithered between the two colours, the way\n         * pixel art shades.\n         */\n        let heat = 0;\n        if (pointer && !still) {\n          const reach = 1 - Math.hypot(x - pointer.x, y - pointer.y) / HEAT_RADIUS;\n          heat = reach > 0 ? reach * reach * (3 - 2 * reach) : 0;\n        } else if (pointer) {\n          const reach = 1 - Math.hypot(x - pointer.x, y - pointer.y) / HEAT_RADIUS;\n          heat = reach > 0 ? 1 : 0;\n        }\n        if (dt > 0) {\n          const rate = heat > cell.heat ? HEAT_RISE : HEAT_FALL;\n          cell.heat += (heat - cell.heat) * (1 - Math.exp(-dt * rate));\n          if (cell.heat < 0.004) cell.heat = 0;\n        }\n        if (cell.heat > 0) anyWarm = true;\n\n        const around = Math.atan2((y - middle.y) / half.y, (x - middle.x) / half.x);\n        const off = Math.atan2(Math.sin(around - band), Math.cos(around - band));\n        const swept = ambient ? Math.exp(-(off * off) / (2 * BAND_WIDTH * BAND_WIDTH)) * 0.85 : 0;\n        const light = Math.min(1, Math.max(digit.accent ? HIGHLIGHT_LIGHT : 0, swept, cell.heat));\n\n        if (isAccent) {\n          // Dissolve's flash, already decided.\n        } else if (light > 0.1 + cell.pick * 0.85) {\n          isAccent = true;\n          alpha *= 0.66 + 0.34 * light;\n        } else {\n          alpha *= (PLAIN_TONES[cell.tone] ?? 0.34) + 0.18 * light;\n        }\n        size *= 0.9 + 0.2 * light + 0.35 * cell.heat;\n\n        add(isAccent, alpha, cell.square, (x + 0.5) * pitch, (y + 0.5) * pitch, (size * pitch) / 2);\n      }\n    }\n\n    warm = anyWarm;\n\n    for (const [key, path] of paths) {\n      context.fillStyle = key[0] === '1' ? accent : plain;\n      context.globalAlpha = Number(key.slice(2)) / ALPHA_STEPS;\n      context.fill(path);\n    }\n    context.globalAlpha = 1;\n  };\n\n  /* ---------------------------------- loop ---------------------------------- */\n\n  const tick = (now: number) => {\n    frame = 0;\n    if (destroyed) return;\n    const dt = last === 0 ? 1 / 60 : Math.min(1 / 30, (now - last) / 1000);\n    last = now;\n    time += dt;\n    const moving = step(dt);\n    const turning = ambient && !reducedMotion.matches;\n    // With only the band moving, 30 frames a second is plenty and halves the work.\n    if (moving || now - lastDraw >= 32) {\n      draw(now - lastDraw > 0 && lastDraw !== 0 ? Math.min(1 / 15, (now - lastDraw) / 1000) : dt);\n      lastDraw = now;\n    }\n    if ((moving || turning) && visible) frame = requestAnimationFrame(tick);\n    else {\n      last = 0;\n      lastDraw = 0;\n    }\n  };\n\n  function invalidate() {\n    if (frame === 0 && !destroyed) frame = requestAnimationFrame(tick);\n  }\n\n  /* --------------------------------- pointer -------------------------------- */\n\n  /** Which digit is nearest the pointer, and how far away it is, in cells. */\n  const nearest = () => {\n    if (!pointer) return { index: -1, distance: Infinity };\n    let index = -1;\n    let distance = Infinity;\n    digits.forEach((digit, i) => {\n      if (digit.cells.length === 0) return;\n      const left = PAD_X + digit.left;\n      const dx = Math.max(left - pointer!.x, 0, pointer!.x - (left + GLYPH_COLUMNS));\n      const dy = Math.max(PAD_Y - pointer!.y, 0, pointer!.y - (PAD_Y + GLYPH_ROWS));\n      const d = Math.hypot(dx, dy);\n      if (d < distance) {\n        distance = d;\n        index = i;\n      }\n    });\n    return { index, distance };\n  };\n\n  const track = (event: PointerEvent) => {\n    if (pitch === 0) return;\n    const rect = canvas.getBoundingClientRect();\n    pointer = {\n      x: (event.clientX - rect.left) / pitch - 0.5,\n      y: (event.clientY - rect.top) / pitch - 0.5,\n    };\n  };\n\n  const onMove = (event: PointerEvent) => {\n    // A finger only counts while it is down, and that is handled below.\n    if (event.pointerType === 'touch') return;\n    track(event);\n    // Anywhere over the canvas the pointer lights cells, so a frame is wanted\n    // even when no digit is close enough to react.\n    if (\n      pointer &&\n      pointer.x > -HEAT_RADIUS &&\n      pointer.x < columns + HEAT_RADIUS &&\n      pointer.y > -HEAT_RADIUS &&\n      pointer.y < GLYPH_ROWS + PAD_Y * 2 + HEAT_RADIUS\n    )\n      invalidate();\n    const { index, distance } = nearest();\n    const current = digits.findIndex((digit) => digit.active);\n    if (index !== -1 && distance <= APPROACH) activate(index);\n    else if (current !== -1 && (index !== current || distance > RELEASE)) releaseAll();\n  };\n\n  const onDown = (event: PointerEvent) => {\n    if (event.pointerType !== 'touch') return;\n    track(event);\n    const { index, distance } = nearest();\n    if (index === -1 || distance > APPROACH) return;\n    window.clearTimeout(touchTimer);\n    activate(index);\n  };\n\n  const onUp = (event: PointerEvent) => {\n    if (event.pointerType !== 'touch') return;\n    window.clearTimeout(touchTimer);\n    touchTimer = window.setTimeout(releaseAll, TOUCH_HOLD);\n  };\n\n  const onLeave = () => {\n    pointer = null;\n    releaseAll();\n  };\n  const onOut = (event: PointerEvent) => {\n    if (event.relatedTarget === null) onLeave();\n  };\n\n  let listening = false;\n  const listen = (on: boolean) => {\n    if (on === listening) return;\n    listening = on;\n    const root = document.documentElement;\n    if (on) {\n      window.addEventListener('pointermove', onMove, { passive: true });\n      window.addEventListener('pointerdown', onDown, { passive: true });\n      window.addEventListener('pointerup', onUp);\n      window.addEventListener('pointercancel', onUp);\n      root.addEventListener('pointerout', onOut);\n      window.addEventListener('blur', onLeave);\n    } else {\n      window.removeEventListener('pointermove', onMove);\n      window.removeEventListener('pointerdown', onDown);\n      window.removeEventListener('pointerup', onUp);\n      window.removeEventListener('pointercancel', onUp);\n      root.removeEventListener('pointerout', onOut);\n      window.removeEventListener('blur', onLeave);\n    }\n  };\n\n  /* ------------------------------ observers -------------------------------- */\n\n  const resize = new ResizeObserver(() => {\n    const next = canvas.clientWidth;\n    if (next === 0) return;\n    const ratio = window.devicePixelRatio || 1;\n    width = next;\n    pitch = width / columns;\n    height = pitch * (GLYPH_ROWS + PAD_Y * 2);\n    canvas.width = Math.round(width * ratio);\n    canvas.height = Math.round(height * ratio);\n    // Straight away: resizing cleared the canvas, and a frame's wait would\n    // show it empty.\n    draw();\n    invalidate();\n  });\n  resize.observe(canvas);\n\n  const theme = new MutationObserver(invalidate);\n  theme.observe(document.documentElement, {\n    attributes: true,\n    attributeFilter: ['class', 'style', 'data-theme'],\n  });\n\n  // Off screen nothing listens and nothing moves. Coming back finishes\n  // whatever was under way.\n  const viewport = new IntersectionObserver(([entry]) => {\n    visible = entry?.isIntersecting ?? true;\n    listen(visible);\n    if (!visible) releaseAll();\n    invalidate();\n  });\n  viewport.observe(canvas);\n\n  reducedMotion.addEventListener('change', invalidate);\n\n  return {\n    setEffect(next) {\n      if (next === effect) return;\n      // Put everything back first, so no digit is left half way into the\n      // previous effect.\n      for (const digit of digits) {\n        digit.active = false;\n        digit.amount = 0;\n        digit.particles = digit.cells;\n        for (const cell of digit.cells) {\n          cell.x = cell.hx;\n          cell.y = cell.hy;\n          cell.vx = 0;\n          cell.vy = 0;\n        }\n      }\n      effect = next;\n      invalidate();\n    },\n    destroy() {\n      destroyed = true;\n      cancelAnimationFrame(frame);\n      window.clearTimeout(touchTimer);\n      listen(false);\n      resize.disconnect();\n      theme.disconnect();\n      viewport.disconnect();\n      reducedMotion.removeEventListener('change', invalidate);\n    },\n  };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Parts                                                                       */\n/* -------------------------------------------------------------------------- */\n\nexport interface NotFoundProps extends ComponentProps<'div'> {\n  /** Draws a faint grid behind everything, fading out towards the edges. On by default. */\n  backdrop?: boolean;\n}\n\nconst GRID_TILE =\n  \"url(\\\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48'%3E%3Cpath d='M0 24.5H48M24.5 0V48' stroke='%23000' stroke-opacity='.45'/%3E%3Cpath d='M20 24.5H29M24.5 20V29' stroke='%23000'/%3E%3C/svg%3E\\\")\";\n\nconst backdropStyle: CSSProperties = {\n  maskImage: `${GRID_TILE}, radial-gradient(closest-side, #000 35%, transparent)`,\n  maskSize: '48px 48px, 100% 100%',\n  maskRepeat: 'repeat, no-repeat',\n  maskPosition: 'center, center',\n  maskComposite: 'intersect',\n  WebkitMaskImage: `${GRID_TILE}, radial-gradient(closest-side, #000 35%, transparent)`,\n  WebkitMaskSize: '48px 48px, 100% 100%',\n  WebkitMaskRepeat: 'repeat, no-repeat',\n  WebkitMaskPosition: 'center, center',\n  WebkitMaskComposite: 'source-in',\n};\n\n/** The column the code, the title, the description and the actions stand in, centred. */\nfunction NotFoundRoot({ backdrop = true, className, children, render, ...props }: NotFoundProps & RenderProp<'div'>) {\n  const slots = notFoundVariants({ backdrop });\n  return useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps(\n      {\n        'data-slot': 'not-found',\n        className: slots.root({ className }),\n        children: (\n          <>\n            {backdrop ? (\n              <span\n                aria-hidden=\"true\"\n                data-slot=\"not-found-backdrop\"\n                className={slots.backdrop()}\n                style={backdropStyle}\n              />\n            ) : null}\n            {children}\n          </>\n        ),\n      },\n      props\n    ),\n  });\n}\nNotFoundRoot.displayName = 'NotFound';\n\nexport interface NotFoundCodeProps extends Omit<ComponentProps<'div'>, 'children' | 'color'> {\n  /** The code to draw. Digits only; any other character leaves a blank the width of a digit. */\n  code?: string;\n  /** How the digit nearest the pointer reacts. */\n  effect?: NotFoundEffect;\n  /** Digits drawn with solid strokes three cells wide, or as their outline. */\n  fill?: 'outline' | 'solid';\n  /**\n   * A band of light that goes slowly round the code on its own. While it is\n   * on, the canvas draws about 30 frames a second when the code is on screen;\n   * turn it off and nothing is drawn until the pointer comes near.\n   */\n  ambient?: boolean;\n  /** The theme role of the accent: the highlighted digit, the band of light and the pointer's light. */\n  color?: NotFoundColor;\n  /**\n   * Which digit is lit in `color` even with nothing else lighting it, counting\n   * from 0. Defaults to the middle one. `null` lights none of them.\n   */\n  highlight?: number | null;\n  /** What a screen reader says for the code. Defaults to \"Error\" and the code. */\n  label?: string;\n}\n\n/** The code, drawn in cells on a canvas. Its box keeps the code's shape before anything is drawn. */\nfunction NotFoundCode({\n  code = '404',\n  effect = 'scatter',\n  fill = 'solid',\n  color = 'info',\n  ambient = true,\n  highlight,\n  label,\n  className,\n  style,\n  render,\n  ...props\n}: NotFoundCodeProps & RenderProp<'div'>) {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const engineRef = useRef<Engine | null>(null);\n  const effectRef = useRef(effect);\n  effectRef.current = effect;\n\n  const lit = highlight === undefined ? (code.length > 1 ? Math.floor((code.length - 1) / 2) : null) : highlight;\n  const { columns } = layoutOf(code);\n  const slots = notFoundVariants({ color });\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n    const engine = createEngine({ canvas, code, fill, highlight: lit, effect: effectRef.current, ambient });\n    engineRef.current = engine;\n    return () => {\n      engine.destroy();\n      engineRef.current = null;\n    };\n  }, [code, fill, lit, ambient]);\n\n  useEffect(() => {\n    engineRef.current?.setEffect(effect);\n  }, [effect]);\n\n  // The canvas reaches past the box on every side by the padding, given as a\n  // share of the box so it scales with it.\n  // A canvas does not stretch between `left` and `right` the way a box does:\n  // it keeps the size of its pixel buffer. So its size is given outright.\n  const canvasStyle: CSSProperties = {\n    left: `${(-PAD_X / columns) * 100}%`,\n    top: `${(-PAD_Y / GLYPH_ROWS) * 100}%`,\n    width: `${(1 + (2 * PAD_X) / columns) * 100}%`,\n    height: `${(1 + (2 * PAD_Y) / GLYPH_ROWS) * 100}%`,\n  };\n\n  return useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps(\n      {\n        'data-slot': 'not-found-code',\n        role: 'img',\n        'aria-label': label ?? `Error ${code}`,\n        className: slots.code({ className }),\n        style: { aspectRatio: `${columns} / ${GLYPH_ROWS}`, ...style },\n        children: (\n          <>\n            <canvas ref={canvasRef} aria-hidden=\"true\" className={slots.canvas()} style={canvasStyle} />\n          </>\n        ),\n      },\n      props\n    ),\n  });\n}\nNotFoundCode.displayName = 'NotFound.Code';\n\nexport interface NotFoundTitleProps extends ComponentProps<'h1'> {\n  /** The heading level. A page that is only this one message wants `h1`. */\n  as?: 'h1' | 'h2' | 'h3';\n}\n\n/** What happened, in a few words. */\nfunction NotFoundTitle({ as: Tag = 'h1', className, ...props }: NotFoundTitleProps) {\n  return <Tag data-slot=\"not-found-title\" className={notFoundVariants().title({ className })} {...props} />;\n}\nNotFoundTitle.displayName = 'NotFound.Title';\n\nexport interface NotFoundDescriptionProps extends ComponentProps<'p'> {}\n\n/** A line on what to do next. */\nfunction NotFoundDescription({ className, render, ...props }: NotFoundDescriptionProps & RenderProp<'p'>) {\n  return useRender({\n    defaultTagName: 'p',\n    render,\n    props: mergeProps(\n      { 'data-slot': 'not-found-description', className: notFoundVariants().description({ className }) },\n      props\n    ),\n  });\n}\nNotFoundDescription.displayName = 'NotFound.Description';\n\nexport interface NotFoundActionsProps extends ComponentProps<'div'> {}\n\n/** The ways out: links or buttons in a row that wraps on a narrow screen. */\nfunction NotFoundActions({ className, render, ...props }: NotFoundActionsProps & RenderProp<'div'>) {\n  return useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps(\n      { 'data-slot': 'not-found-actions', className: notFoundVariants().actions({ className }) },\n      props\n    ),\n  });\n}\nNotFoundActions.displayName = 'NotFound.Actions';\n\nexport const NotFound = Object.assign(NotFoundRoot, {\n  Code: NotFoundCode,\n  Title: NotFoundTitle,\n  Description: NotFoundDescription,\n  Actions: NotFoundActions,\n});\n\n/*\n * The parts again, by name, for a page that is a Server Component: it cannot\n * read `NotFound.Code` off a client module, only pass the import along.\n */\nexport { NotFoundCode, NotFoundTitle, NotFoundDescription, NotFoundActions };\n"
    },
    {
      "path": "components/panelui/not-found/glyphs.ts",
      "type": "registry:component",
      "target": "@components/panelui/not-found/glyphs.ts",
      "content": "/**\n * The shapes NotFound draws, as grids of cells.\n *\n * A digit is drawn at 5 by 7 and scaled up three times, so every stroke is\n * three cells wide. Kept whole that is a solid pixel digit; reduced to the\n * cells on its edge it is a digit drawn in outline, two rows of dots with a\n * gap between, which is the look a stroked numeral has at this size.\n *\n * Digits are drawn without diagonal strokes. Two blocks that meet only at a\n * corner come apart when reduced to their edges, so a slanted stroke would\n * outline as a stair of separate rings.\n *\n * The icons Scramble turns a digit into are drawn straight onto the same 15 by\n * 21 grid, from circles and lines, so they arrive at exactly the size of the\n * digit they replace and need no scaling of their own.\n */\n\n/** Cells across one glyph. */\nexport const GLYPH_COLUMNS = 15;\n/** Cells down one glyph. */\nexport const GLYPH_ROWS = 21;\n\nconst SCALE = 3;\n\nconst DIGITS: Record<string, readonly string[]> = {\n  '0': ['.###.', '#...#', '#...#', '#...#', '#...#', '#...#', '.###.'],\n  '1': ['.##..', '..#..', '..#..', '..#..', '..#..', '..#..', '.###.'],\n  '2': ['.###.', '#...#', '....#', '.###.', '#....', '#....', '#####'],\n  '3': ['####.', '....#', '....#', '.###.', '....#', '....#', '####.'],\n  '4': ['#..#.', '#..#.', '#..#.', '#####', '...#.', '...#.', '...#.'],\n  '5': ['#####', '#....', '####.', '....#', '....#', '....#', '####.'],\n  '6': ['.###.', '#....', '#....', '####.', '#...#', '#...#', '.###.'],\n  '7': ['#####', '....#', '....#', '...##', '...#.', '...#.', '...#.'],\n  '8': ['.###.', '#...#', '#...#', '.###.', '#...#', '#...#', '.###.'],\n  '9': ['.###.', '#...#', '#...#', '.####', '....#', '....#', '.###.'],\n};\n\n/** A glyph: which cells are on, row by row. */\nexport type Glyph = boolean[][];\n\nfunction empty(): Glyph {\n  return Array.from({ length: GLYPH_ROWS }, () => Array<boolean>(GLYPH_COLUMNS).fill(false));\n}\n\n/** A digit as a solid grid of cells, or `null` for a character there is no drawing of. */\nfunction solidDigit(character: string): Glyph | null {\n  const rows = DIGITS[character];\n  if (!rows) return null;\n  const glyph = empty();\n  for (let y = 0; y < GLYPH_ROWS; y += 1) {\n    for (let x = 0; x < GLYPH_COLUMNS; x += 1) {\n      glyph[y]![x] = rows[Math.floor(y / SCALE)]![Math.floor(x / SCALE)] === '#';\n    }\n  }\n  return glyph;\n}\n\n/**\n * Only the cells of a glyph that touch a cell outside it, corners included.\n * Counting corners keeps a diagonal stroke as a continuous band; counting\n * only sides leaves it as a scatter of single dots.\n */\nfunction outlineOf(glyph: Glyph): Glyph {\n  const on = (x: number, y: number) => glyph[y]?.[x] === true;\n  return glyph.map((row, y) =>\n    row.map((cell, x) => {\n      if (!cell) return false;\n      for (let dy = -1; dy <= 1; dy += 1) {\n        for (let dx = -1; dx <= 1; dx += 1) if (!on(x + dx, y + dy)) return true;\n      }\n      return false;\n    })\n  );\n}\n\n/** A digit, solid or as its outline. */\nexport function digitGlyph(character: string, fill: 'outline' | 'solid'): Glyph | null {\n  const solid = STROKED[character] ?? solidDigit(character);\n  if (!solid) return null;\n  return fill === 'solid' ? solid : outlineOf(solid);\n}\n\n/* -------------------------------------------------------------------------- */\n/* Icons                                                                       */\n/* -------------------------------------------------------------------------- */\n\n/** Distance from a point to the segment from a to b. */\nfunction toSegment(px: number, py: number, ax: number, ay: number, bx: number, by: number) {\n  const dx = bx - ax;\n  const dy = by - ay;\n  const t = Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / (dx * dx + dy * dy || 1)));\n  return Math.hypot(px - (ax + dx * t), py - (ay + dy * t));\n}\n\n/** A glyph from a test of each cell's centre. */\nfunction drawn(test: (x: number, y: number) => boolean): Glyph {\n  const glyph = empty();\n  for (let y = 0; y < GLYPH_ROWS; y += 1) {\n    for (let x = 0; x < GLYPH_COLUMNS; x += 1) glyph[y]![x] = test(x + 0.5, y + 0.5);\n  }\n  return glyph;\n}\n\n/** A ring: cells whose centre is within half a stroke of the circle. */\nconst ring = (x: number, y: number, cx: number, cy: number, r: number, stroke: number) =>\n  Math.abs(Math.hypot(x - cx, y - cy) - r) <= stroke / 2;\n\n/** A question mark: a hook, a short stem, a dot. */\nconst question = drawn((x, y) => {\n  const angle = Math.atan2(y - 6.5, x - 7.5);\n  // The hook runs from the left of the circle, over the top, round to the\n  // lower right, and stops there so the stem can drop from it.\n  const onHook = ring(x, y, 7.5, 6.5, 4.6, 2) && (y < 6.5 || angle < Math.PI * 0.35);\n  const stem = toSegment(x, y, 9.4, 10.6, 7.5, 13.2) <= 1 || toSegment(x, y, 7.5, 13.2, 7.5, 14.5) <= 1;\n  const dot = Math.hypot(x - 7.5, y - 18) <= 1.3;\n  return onHook || stem || dot;\n});\n\n/** A magnifying glass, the lens high and the handle running to the lower right. */\nconst magnifier = drawn(\n  (x, y) => ring(x, y, 6.5, 7.5, 4.8, 2) || toSegment(x, y, 10.2, 11.6, 13.4, 17.6) <= 1.2\n);\n\n/** A map pin: a ring with a dot in it, narrowing to a point. */\nconst pin = drawn((x, y) => {\n  const head = ring(x, y, 7.5, 7.5, 5, 2) && y < 10.5;\n  // The two sides run from the widest part of the ring down to the point.\n  const sides =\n    toSegment(x, y, 3, 9.6, 7.5, 18.6) <= 1 || toSegment(x, y, 12, 9.6, 7.5, 18.6) <= 1;\n  const dot = Math.hypot(x - 7.5, y - 7.5) <= 1.5;\n  return head || sides || dot;\n});\n\n/*\n * Digits drawn from strokes rather than scaled up, for the ones whose shape\n * needs a diagonal. Each stroke is three cells wide, the same as a scaled-up\n * pixel, so they sit with the others.\n */\nconst stroke = (x: number, y: number, ax: number, ay: number, bx: number, by: number) =>\n  toSegment(x, y, ax, ay, bx, by) <= 1.5;\n\n/** Distance from a point to an ellipse's outline, near enough for drawing at this size. */\nconst toEllipse = (x: number, y: number, cx: number, cy: number, rx: number, ry: number) => {\n  const nx = (x - cx) / rx;\n  const ny = (y - cy) / ry;\n  const r = Math.hypot(nx, ny);\n  // Scale the radial error back to cells by the radius in that direction.\n  const along = r === 0 ? Math.min(rx, ry) : Math.hypot(nx * rx, ny * ry) / r;\n  return Math.abs(r - 1) * along;\n};\n\nconst STROKED: Record<string, Glyph> = {\n  // An oval nought, not a box with its corners cut.\n  '0': drawn((x, y) => toEllipse(x, y, 7.5, 10.5, 6, 9) <= 1.5),\n  // A closed four: the diagonal runs from the top of the stem down to the\n  // crossbar, and the crossbar reaches a little past the stem.\n  '4': drawn(\n    (x, y) =>\n      stroke(x, y, 10.5, 1.5, 10.5, 19.5) ||\n      stroke(x, y, 10.5, 1.5, 1.5, 14.5) ||\n      stroke(x, y, 1.5, 14.5, 13.5, 14.5)\n  ),\n};\n\n/** The icons Scramble cycles through, in order. */\nexport const ICONS: readonly Glyph[] = [question, magnifier, pin];\n"
    }
  ]
}
