{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "field-style",
  "title": "Aurora field style",
  "description": "Browser colour decoder and shared aurora field types.",
  "files": [
    {
      "path": "components/aurora/fieldStyle.ts",
      "content": "/** Reads the aurora tokens (--au-*) where a canvas sits. */\nimport type { FieldPalette, Rgb } from './fieldTypes';\n\nlet decoder: CanvasRenderingContext2D | null;\n\n/** The browser resolves CSS colours once when the palette changes. */\nexport function readTriplet(style: CSSStyleDeclaration, name: string, previous: Rgb | string): Rgb {\n  const fallback: Rgb =\n    typeof previous === 'string' ? (previous.split(',').map(Number) as Rgb) : previous;\n  const raw = style.getPropertyValue(name).trim();\n  if (!raw) return fallback;\n  const colour = CSS.supports('color', raw) ? raw : `rgb(${raw})`;\n  if (!CSS.supports('color', colour)) {\n    console.error(`Invalid aurora colour ${name}: ${raw}`);\n    return fallback;\n  }\n  if (!decoder) {\n    const canvas = document.createElement('canvas');\n    canvas.width = canvas.height = 1;\n    decoder = canvas.getContext('2d', { willReadFrequently: true });\n  }\n  if (!decoder) return fallback;\n  decoder.clearRect(0, 0, 1, 1);\n  decoder.fillStyle = colour;\n  decoder.fillRect(0, 0, 1, 1);\n  const pixel = decoder.getImageData(0, 0, 1, 1).data;\n  return [pixel[0], pixel[1], pixel[2]];\n}\n\n/** The dark theme: this site's body class, or shadcn's .dark on <html>. */\nfunction isDarkTheme() {\n  return (\n    document.body.classList.contains('next-dark-theme') ||\n    document.documentElement.classList.contains('dark')\n  );\n}\n\n/** Reads the palette where the canvas sits, so a section can override the --au-dot* tokens locally. */\nexport function readPalette(element: Element, previous?: FieldPalette): FieldPalette {\n  const style = getComputedStyle(element);\n  return {\n    hues: [\n      readTriplet(style, '--au-dot', previous?.hues[0] ?? [82, 177, 150]),\n      readTriplet(style, '--au-dot-2', previous?.hues[1] ?? [82, 177, 150]),\n      readTriplet(style, '--au-dot-3', previous?.hues[2] ?? [82, 177, 150]),\n      readTriplet(style, '--au-dot-4', previous?.hues[3] ?? [82, 177, 150]),\n    ],\n    dark: isDarkTheme(),\n  };\n}\n",
      "type": "registry:component",
      "target": "components/aurora/fieldStyle.ts"
    },
    {
      "path": "components/aurora/fieldTypes.ts",
      "content": "import type { QuietZone } from './quietMask';\n\nexport type Rgb = [number, number, number];\nexport type FieldContext = CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;\n\nexport function fieldCanvas(width: number, height: number) {\n  if (typeof document === 'undefined') return new OffscreenCanvas(width, height);\n  const canvas = document.createElement('canvas');\n  canvas.width = width;\n  canvas.height = height;\n  return canvas;\n}\n\nexport interface FieldPalette {\n  /** Aurora colours by altitude: green, violet, rose, gold. */\n  hues: [Rgb, Rgb, Rgb, Rgb];\n  /** True when the field sits on a night ground (the dark theme). */\n  dark: boolean;\n}\n\n/** Everything the curtain needs to paint one frame. */\nexport interface EngineInput {\n  ctx: FieldContext;\n  width: number;\n  height: number;\n  /** Seconds since the field started. */\n  t: number;\n  quiet: QuietZone;\n  palette: FieldPalette;\n  placement?: 'footer';\n}\n\nexport const TAU = Math.PI * 2;\n\nexport const rgba = (rgb: Rgb, alpha: number) => `rgba(${rgb.join(',')}, ${alpha})`;\n\n/** Linear blend from a (k = 0) to b (k = 1), rounded to whole channel values. */\nexport const mix = (a: Rgb, b: Rgb, k: number): Rgb => [\n  Math.round(a[0] + (b[0] - a[0]) * k),\n  Math.round(a[1] + (b[1] - a[1]) * k),\n  Math.round(a[2] + (b[2] - a[2]) * k),\n];\n",
      "type": "registry:component",
      "target": "components/aurora/fieldTypes.ts"
    },
    {
      "path": "components/aurora/quietMask.ts",
      "content": "import { type FieldContext, fieldCanvas } from './fieldTypes';\n\n/** Where text sits on top of the curtain; the light thins out there. */\nexport type QuietZone = 'top-left' | 'left' | 'center' | 'none';\n\nconst smooth = (edge0: number, edge1: number, v: number) => {\n  const k = Math.max(0, Math.min(1, (v - edge0) / (edge1 - edge0)));\n  return k * k * (3 - 2 * k);\n};\n\n/** How much light stays at (x, y), 0 to 1. Narrow screens stack copy full width, so the quiet zone becomes the top band. */\nfunction quietFactor(zone: QuietZone, x: number, y: number, narrow: boolean): number {\n  if (zone === 'none') return 1;\n  if (narrow) return zone === 'center' ? 1 : 0.05 + 0.95 * smooth(0.62, 0.86, y);\n  switch (zone) {\n    case 'top-left':\n      return 1 - 0.9 * (1 - smooth(0.44, 0.64, x)) * (1 - smooth(0.42, 0.6, y));\n    case 'left':\n      return 0.04 + 0.96 * smooth(0.42, 0.72, x);\n    case 'center':\n      return 0.25 + 0.75 * smooth(0.18, 0.42, Math.abs(x - 0.5));\n  }\n}\n\n/* The mask is resolution independent: a small alpha map, scaled up with smoothing. */\nconst MASK_W = 64;\nconst MASK_H = 40;\nconst cache = new Map<string, HTMLCanvasElement | OffscreenCanvas>();\n\nfunction buildMask(zone: QuietZone, narrow: boolean) {\n  const canvas = fieldCanvas(MASK_W, MASK_H);\n  const ctx = canvas.getContext('2d') as FieldContext | null;\n  if (!ctx) return canvas;\n  const image = ctx.createImageData(MASK_W, MASK_H);\n  for (let row = 0; row < MASK_H; row += 1) {\n    const y = (row + 0.5) / MASK_H;\n    for (let column = 0; column < MASK_W; column += 1) {\n      const x = (column + 0.5) / MASK_W;\n      const keep = quietFactor(zone, x, y, narrow);\n      image.data[(row * MASK_W + column) * 4 + 3] = Math.round((1 - keep) * 255);\n    }\n  }\n  ctx.putImageData(image, 0, 0);\n  return canvas;\n}\n\n/** Erases the curtain where text sits (the quiet zone), so the copy stays readable. */\nexport function applyQuiet(ctx: FieldContext, width: number, height: number, zone: QuietZone) {\n  if (zone === 'none') return;\n  const narrow = width / height < 0.9;\n  const key = `${zone}:${narrow}`;\n  let mask = cache.get(key);\n  if (!mask) {\n    mask = buildMask(zone, narrow);\n    cache.set(key, mask);\n  }\n  ctx.save();\n  ctx.globalCompositeOperation = 'destination-out';\n  ctx.imageSmoothingEnabled = true;\n  ctx.drawImage(mask, 0, 0, width, height);\n  ctx.restore();\n}\n",
      "type": "registry:component",
      "target": "components/aurora/quietMask.ts"
    }
  ],
  "type": "registry:component"
}