{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "weather",
  "type": "registry:ui",
  "title": "Weather",
  "description": "Weather as an object: a slab with a dot-matrix sky sunk into it that follows the time of day, the temperature in the pixel face on its ground, the next hours and the week as dot ranges on one scale; and a small tile of one sky.",
  "dependencies": [],
  "registryDependencies": [
    "https://metalui.dev/r/tokens.json"
  ],
  "files": [
    {
      "path": "packages/metalui/src/blocks/weather/weather.tsx",
      "type": "registry:ui",
      "target": "components/metalui/blocks/weather/weather.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\nimport { Surface } from '../../components/surface/surface';\nimport { Well } from '../../components/well/well';\nimport { Led } from '../../components/led/led';\nimport { Rule } from '../../components/rule/rule';\n\n/* ─────────────────────────────────────────────────────────\n * WEATHER: a custom block (an object). A raise slab with a dot-matrix sky sunk into a field well.\n *\n *   Weather.Root › Weather.Header (place, summary, LED, clock)\n *                › Weather.Sky (the well and its scene; children sit on its ground) › Weather.Now\n *                › Weather.Hours (dot glyph and temperature per slot)\n *                › Weather.Week (each day's low to high as lit dots on one shared scale)\n *   Weather      all of the above from props, in that order\n *   WeatherTile  Weather.Root(tile) › Weather.Sky(tile) › Weather.Now(tile)\n *\n * The sky is a description, not a picture: a `WeatherSky` names the time of day, the moon's\n * phase, the clouds (where, how big, how dark), rain, snow, thunder, mist, wind (strength and\n * direction), heat and birds, and the scene is drawn from it on the recipe's 8 pitch. `WEATHER_SKIES`\n * holds a sky per `WeatherKind`; spread one and change what you need ({ ...WEATHER_SKIES.snow,\n * wind: 0.9 } is a blizzard). The sun rides its arc to the hour and dawn and dusk warm the horizon;\n * after dark the moon, in its phase, crosses and the stars twinkle. Everything that moves steps one\n * frame every dot.frame; reduced motion, or animate={false}, holds one frame.\n * ───────────────────────────────────────────────────────── */\n\nexport type WeatherKind = 'clear' | 'partly' | 'cloud' | 'rain' | 'drizzle' | 'storm' | 'snow' | 'sleet' | 'mist' | 'windy' | 'heat';\n\n/** A cloud in the sky: its centre (0–1 across and down to the horizon), its half-width in dots, and whether it is dark. */\nexport interface WeatherCloud {\n  x: number;\n  y: number;\n  size: number;\n  dark?: boolean;\n}\n\n/** What the sky shows. Every field is optional; amounts run 0–1. */\nexport interface WeatherSky {\n  /** The clouds, drifting with the wind. */\n  clouds?: WeatherCloud[];\n  /** The clouds hide the sun, the moon and the stars. */\n  overcast?: boolean;\n  /** Rain under the clouds: 0.3 drizzle, 0.6 rain, 1 a downpour. */\n  rain?: number;\n  /** Snow falling across the sky. */\n  snow?: number;\n  /** Lightning from the clouds every few seconds. */\n  thunder?: boolean;\n  /** Bands of mist over the ground. */\n  mist?: number;\n  /** How hard the wind blows: the clouds drift faster and gusts streak across. */\n  wind?: number;\n  /** Which way the wind blows (default 1, left to right). */\n  windFrom?: 1 | -1;\n  /** Heat shimmering off the ground. */\n  heat?: number;\n  /** Birds crossing a daytime sky. */\n  birds?: number;\n  /** The moon's age, 0–1 (0 new, 0.5 full). Default: the phase on `date`, or full. */\n  moonPhase?: number;\n}\n\n/** A sky for each kind; spread one and change what you need. */\nexport const WEATHER_SKIES: Record<WeatherKind, WeatherSky> = {\n  clear: { birds: 2 },\n  partly: { clouds: [{ x: 0.18, y: 0.24, size: 3 }, { x: 0.62, y: 0.42, size: 5 }] },\n  cloud: { overcast: true, clouds: [{ x: 0.1, y: 0.24, size: 4 }, { x: 0.45, y: 0.4, size: 5, dark: true }, { x: 0.8, y: 0.2, size: 4 }, { x: 0.65, y: 0.62, size: 3 }] },\n  drizzle: { overcast: true, rain: 0.3, clouds: [{ x: 0.3, y: 0.2, size: 5 }, { x: 0.75, y: 0.28, size: 4 }] },\n  rain: { overcast: true, rain: 0.6, clouds: [{ x: 0.22, y: 0.2, size: 5, dark: true }, { x: 0.68, y: 0.3, size: 5, dark: true }] },\n  storm: { overcast: true, rain: 1, thunder: true, wind: 0.5, clouds: [{ x: 0.12, y: 0.18, size: 6, dark: true }, { x: 0.52, y: 0.24, size: 6, dark: true }, { x: 0.92, y: 0.14, size: 5, dark: true }] },\n  snow: { overcast: true, snow: 0.8, clouds: [{ x: 0.25, y: 0.14, size: 5 }, { x: 0.72, y: 0.2, size: 4 }] },\n  sleet: { overcast: true, snow: 0.4, rain: 0.4, clouds: [{ x: 0.25, y: 0.16, size: 5, dark: true }, { x: 0.7, y: 0.22, size: 4 }] },\n  mist: { mist: 0.8 },\n  windy: { wind: 0.9, clouds: [{ x: 0.4, y: 0.26, size: 3 }] },\n  heat: { heat: 1 },\n};\n\n/** One slot of the next hours. `temp` is in the unit you show. */\nexport interface WeatherHour {\n  /** \"Now\", \"12\", \"15\"… */\n  label: string;\n  /** The hour of day it stands for (0–24), so night hours draw the moon. */\n  hour: number;\n  kind: WeatherKind;\n  temp: number;\n}\n\n/** One day of the week. `low` and `high` are in the unit you show. */\nexport interface WeatherDay {\n  /** \"Today\", \"Sun\", \"Mon\"… */\n  name: string;\n  kind: WeatherKind;\n  low: number;\n  high: number;\n}\n\n/* ── the sky ─────────────────────────────────────────────── */\n\nconst PITCH = 8;\nconst DOT = 6;\nconst FRAME_MS = 166;\nconst SUNRISE = 7.5;\nconst SUNSET = 19.5;\nconst FULL_MOON = Date.UTC(2026, 8, 26, 16, 49);\nconst SYNODIC = 29.530589;\nconst MS_PER_DAY = 86400000;\n\n/** The moon's age on a date, 0–1 (0 new, 0.5 full). */\nexport function moonAge(date: Date) {\n  const age = ((date.getTime() - FULL_MOON) / MS_PER_DAY / SYNODIC + 0.5) % 1;\n  return age < 0 ? age + 1 : age;\n}\n\n/** The phase's name for an age: \"Full moon\", \"Waxing crescent\"… */\nexport function moonPhaseName(age: number) {\n  return ['New moon', 'Waxing crescent', 'First quarter', 'Waxing gibbous', 'Full moon', 'Waning gibbous', 'Last quarter', 'Waning crescent'][Math.round(age * 8) % 8];\n}\n\n/** Whether a dot at (dx, dy) of a disc of radius r is lit at a moon age. */\nfunction moonLit(dx: number, r: number, age: number) {\n  const lit = (1 - Math.cos(age * 2 * Math.PI)) / 2;\n  const nx = dx / r;\n  return age < 0.5 ? nx >= 1 - 2 * lit : nx <= -(1 - 2 * lit);\n}\n\nexport type WeatherSkyLayer =\n  | 'off' | 'glow' | 'star' | 'moonDark' | 'moon' | 'sun' | 'ray' | 'heat' | 'bird' | 'snow'\n  | 'rain' | 'bolt' | 'cloud' | 'cloudDark' | 'fog' | 'wind' | 'hill' | 'hz';\nexport const WEATHER_SKY_LAYERS: WeatherSkyLayer[] = ['off', 'glow', 'star', 'moonDark', 'moon', 'sun', 'ray', 'heat', 'bird', 'snow', 'rain', 'bolt', 'cloud', 'cloudDark', 'fog', 'wind', 'hill', 'hz'];\nconst SKY_PAINT: Record<WeatherSkyLayer, string> = {\n  off: 'fill-weather-ink-off',\n  glow: 'fill-weather-ink-sun opacity-weather-ink-glow-opacity',\n  star: 'fill-weather-ink-star',\n  moonDark: 'fill-weather-ink-moon opacity-weather-ink-moon-dark-opacity',\n  moon: 'fill-weather-ink-moon',\n  sun: 'fill-weather-ink-sun',\n  ray: 'fill-weather-ink-sun',\n  heat: 'fill-weather-ink-sun opacity-weather-ink-heat-opacity',\n  bird: 'fill-weather-ink-hill',\n  snow: 'fill-weather-ink-snow',\n  rain: 'fill-weather-ink-rain',\n  bolt: 'fill-weather-ink-sun',\n  cloud: 'fill-weather-ink-cloud',\n  cloudDark: 'fill-weather-ink-cloud-dark',\n  fog: 'fill-weather-ink-cloud opacity-weather-ink-fog-opacity',\n  wind: 'fill-weather-ink-cloud-dark',\n  hill: 'fill-weather-ink-hill',\n  hz: 'fill-weather-ink-hz',\n};\n\nconst wrap = (v: number, n: number) => ((v % n) + n) % n;\nconst clamp = (v: number | undefined) => Math.min(1, Math.max(0, v ?? 0));\n\nexport interface WeatherSceneOptions {\n  cols: number;\n  rows: number;\n  /** The horizon's row; the ground below it is left for words. */\n  horizon: number;\n  /** The time of day in hours. */\n  hour: number;\n  sky: WeatherSky;\n  /** The frame. */\n  tick?: number;\n}\n\n/**\n * One frame of a dot-matrix sky, as an SVG path per layer on an 8 pitch (6 dots). Draw them in\n * WEATHER_SKY_LAYERS order; Weather.Sky does, and you can too for a sky of your own size.\n */\nexport function weatherScene({ cols, rows, horizon: hz, hour, sky, tick = 0 }: WeatherSceneOptions) {\n  const grid = new Map<string, WeatherSkyLayer>();\n  const big = cols > 30;\n  const put = (x: number, y: number, layer: WeatherSkyLayer) => {\n    x = Math.round(x);\n    y = Math.round(y);\n    if (x < 0 || y < 0 || x >= cols || y >= hz) return;\n    grid.set(`${x},${y}`, layer);\n  };\n  const h = wrap(hour, 24);\n  const day = h >= SUNRISE && h <= SUNSET;\n  const td = (h - SUNRISE) / (SUNSET - SUNRISE);\n  const tn = wrap(h - SUNSET, 24) / (24 - (SUNSET - SUNRISE));\n  const arc = (t: number) => [3 + t * (cols - 7), hz - 1 - Math.sin(Math.PI * t) * (hz - (big ? 8 : 6))];\n  const rain = clamp(sky.rain), snow = clamp(sky.snow), mist = clamp(sky.mist), wind = clamp(sky.wind), heat = clamp(sky.heat);\n  const dir = sky.windFrom ?? 1;\n  const covered = !!sky.overcast;\n\n  // Dawn and dusk warm the dots along the horizon.\n  if (day && (td < 0.1 || td > 0.9) && !covered) {\n    for (let y = hz - 4; y < hz; y++) for (let x = 0; x < cols; x++) {\n      if ((x + y) % 2 === 0 && (y >= hz - 2 || x % 4 === 0)) put(x, y, 'glow');\n    }\n  }\n  if (!day && !covered) {\n    for (let s = 0; s < (big ? 14 : 6); s++) {\n      if (wrap(tick + s * 3, 9) !== 0) put(wrap(s * 17 + 5, cols), wrap(s * 7 + 1, hz - 4), 'star');\n    }\n  }\n  if (!covered) {\n    if (day) {\n      const r = big ? 3.5 : 2.6;\n      const [px, py] = arc(td);\n      const sx = Math.round(px), sy = Math.round(py);\n      for (let dx = -4; dx <= 4; dx++) for (let dy = -4; dy <= 4; dy++) if (dx * dx + dy * dy <= r * r + 0.3) put(sx + dx, sy + dy, 'sun');\n      const dirs = wrap(tick, 4) < 2 ? [[0, -1], [0, 1], [-1, 0], [1, 0]] : [[-0.72, -0.72], [0.72, -0.72], [-0.72, 0.72], [0.72, 0.72]];\n      for (const [ddx, ddy] of dirs) for (let k = r + 1.6; k <= r + 2.8; k += 1) put(sx + ddx * k, sy + ddy * k, 'ray');\n      for (let b = 0; b < Math.min(3, Math.round(sky.birds ?? 0)) && (big || b < 1); b++) {\n        const bx = wrap(Math.floor(cols * 0.55) + b * 7 + dir * Math.floor(tick / 2), cols + 4) - 2;\n        const by = (big ? 6 : 4) + b * 3;\n        const up = wrap(tick, 2) === 0 ? 1 : 0;\n        put(bx - 1, by - up, 'bird');\n        put(bx, by, 'bird');\n        put(bx + 1, by - up, 'bird');\n      }\n    } else {\n      // The moon in its phase: lit dots and the faint rest of the disc.\n      const r = big ? 3.2 : 2.3;\n      const age = sky.moonPhase ?? 0.5;\n      const [qx, qy] = arc(tn);\n      const mx = Math.round(qx), my = Math.round(qy);\n      for (let dx = -4; dx <= 4; dx++) for (let dy = -4; dy <= 4; dy++) {\n        if (dx * dx + dy * dy > r * r + 0.4) continue;\n        put(mx + dx, my + dy, moonLit(dx, r, age) ? 'moon' : 'moonDark');\n      }\n    }\n  }\n  if (heat > 0) {\n    for (let x = 1; x < cols; x += Math.max(2, Math.round(6 - heat * 2))) for (let y = hz - Math.round(3 + heat * 4); y < hz; y++) {\n      if (wrap(y + tick, 3) === 0) put(x + wrap(y + tick, 2), y, 'heat');\n    }\n  }\n  if (snow > 0) {\n    const flakes = Math.round(snow * (big ? 42 : 15));\n    for (let f = 0; f < flakes; f++) {\n      const sway = Math.round(Math.sin(tick / 3 + f)) + dir * Math.round(wind * 3 * Math.floor(tick / 2) / 2);\n      put(wrap(f * 13 + sway, cols), wrap(f * 7 + Math.floor(tick / 2), hz), 'snow');\n    }\n  }\n\n  const drift = dir * tick * (0.25 + wind * 0.7);\n  const clouds = (sky.clouds ?? []).map((c) => {\n    const w = Math.max(2, Math.round(c.size * (0.55 + (0.45 * cols) / 46)));\n    const cx = wrap(Math.round(c.x * cols + drift), cols + 2 * w + 4) - w - 2;\n    return { cx, cy: Math.round(c.y * hz), w, layer: (c.dark ? 'cloudDark' : 'cloud') as WeatherSkyLayer };\n  });\n  if (rain > 0) {\n    const step = rain > 0.7 ? 1 : 2;\n    const fast = rain > 0.7 ? 2 : 1;\n    const gap = rain < 0.4 ? 6 : 4;\n    const slant = rain > 0.7 || wind > 0.4;\n    for (const { cx, cy, w } of clouds) for (let x = cx - w + 1; x <= cx + w - 1; x++) {\n      if (wrap(x, step) !== 0) continue;\n      for (let y = cy + 2; y < hz; y++) if (wrap(y - tick * fast + x * 3, gap) === 0) put(x - (slant ? dir * wrap(y, 2) : 0), y, 'rain');\n    }\n  }\n  if (sky.thunder && clouds.length && wrap(tick, 18) < 3 && wrap(tick, 18) !== 1) {\n    const c0 = clouds[1] ?? clouds[0];\n    let lx = c0.cx;\n    for (let y = c0.cy + 2, step = 0; y < hz; y++, step++) {\n      lx += wrap(step, 6) < 3 ? -1 : 1;\n      put(lx, y, 'bolt');\n      put(lx + 1, y, 'bolt');\n    }\n  }\n  for (const { cx, cy, w, layer } of clouds) {\n    for (let x = cx - w + 1; x <= cx + w - 1; x++) put(x, cy + 1, layer);\n    for (let x = cx - w; x <= cx + w; x++) put(x, cy, layer);\n    for (let x = cx - w + 1; x <= cx + w - 2; x++) put(x, cy - 1, layer);\n    for (let x = cx - 1; x <= cx + 2; x++) put(x, cy - 2, layer);\n    put(cx - w + 2, cy - 2, layer);\n    put(cx - w + 3, cy - 2, layer);\n    put(cx, cy - 3, layer);\n    put(cx + 1, cy - 3, layer);\n  }\n  if (mist > 0) {\n    const bands = (big ? [2, 5, 8, 11] : [2, 4, 7]).slice(0, Math.max(1, Math.round(mist * (big ? 4 : 3))));\n    bands.forEach((d, i) => {\n      const way = i % 2 ? -1 : 1;\n      for (let x = 0; x < cols; x++) if (wrap(x + way * Math.floor(tick / 2) + i * 3, 9) < 5) put(x, hz - d, 'fog');\n    });\n  }\n  if (wind > 0.3) {\n    const gusts = Math.round(wind * (big ? 8 : 4));\n    for (let g = 0; g < gusts; g++) {\n      const wy = 2 + wrap(g * 5, hz - 4), len = 3 + (g % 3);\n      const wx = dir > 0 ? wrap(g * 17 + tick * 2, cols + 8) - 4 : cols - (wrap(g * 17 + tick * 2, cols + 8) - 4) - len;\n      for (let k = 0; k < len; k++) put(wx + k, wy, 'wind');\n      put(dir > 0 ? wx + len : wx - 1, wy - 1, 'wind');\n    }\n  }\n  // The hills on the horizon.\n  for (let x = 0; x < cols; x++) {\n    const hill = Math.max(0, Math.round(3 - Math.abs(x - cols * 0.8) / 2.4)) + Math.max(0, Math.round(1.6 - Math.abs(x - cols * 0.12) / 2.2));\n    for (let y = hz - hill; y < hz; y++) grid.set(`${x},${y}`, 'hill');\n    grid.set(`${x},${hz}`, 'hz');\n  }\n\n  const out = Object.fromEntries(WEATHER_SKY_LAYERS.map((l) => [l, ''])) as Record<WeatherSkyLayer, string>;\n  const o = (PITCH - DOT) / 2;\n  for (let y = 0; y < rows; y++) for (let x = 0; x < cols; x++) {\n    out[grid.get(`${x},${y}`) ?? 'off'] += `M${x * PITCH + o} ${y * PITCH + o}h${DOT}v${DOT}h-${DOT}Z`;\n  }\n  return out;\n}\n\n/* ── small dot glyphs for the hours and the week ─────────── */\n\ntype GlyphLayer = 'sun' | 'moon' | 'cloud' | 'cloudDark' | 'rain' | 'bolt' | 'fog' | 'snow' | 'wind';\nconst GLYPH_LAYERS: GlyphLayer[] = ['sun', 'moon', 'cloud', 'cloudDark', 'rain', 'bolt', 'fog', 'snow', 'wind'];\nconst GLYPH_PAINT: Record<GlyphLayer, string> = {\n  sun: 'fill-weather-ink-sun',\n  moon: 'fill-weather-ink-moon',\n  cloud: 'fill-weather-ink-cloud',\n  cloudDark: 'fill-weather-ink-cloud-dark',\n  rain: 'fill-weather-ink-rain',\n  bolt: 'fill-weather-ink-sun',\n  fog: 'fill-weather-ink-cloud',\n  snow: 'fill-weather-ink-snow',\n  wind: 'fill-weather-ink-cloud-dark',\n};\n// 7 × 7 dots. S sun, M moon, C cloud, D dark cloud, R rain, Z bolt, F fog, N snow, W wind.\nconst MINI: Record<string, string[]> = {\n  sun: ['...S...', '.S...S.', '..SSS..', 'S.SSS.S', '..SSS..', '.S...S.', '...S...'],\n  moon: ['..MMM..', '.MM....', 'MM.....', 'MM.....', 'MM.....', '.MM....', '..MMM..'],\n  partly: ['S.S....', '.SSS...', 'SSSCC..', '.SCCCC.', 'CCCCCCC', '.CCCCC.', '.......'],\n  partlyNight: ['MM.....', 'M......', 'M..CC..', 'MMCCCC.', 'CCCCCCC', '.CCCCC.', '.......'],\n  cloud: ['.......', '..CC...', '.CCCCC.', 'CCCCCCC', 'CCCCCCC', '.CCCCC.', '.......'],\n  drizzle: ['..CC...', '.CCCCC.', 'CCCCCCC', '.CCCCC.', '.......', '.R...R.', '...R...'],\n  rain: ['..DD...', '.DDDDD.', 'DDDDDDD', '.DDDDD.', '.......', '.R.R.R.', 'R.R.R..'],\n  storm: ['..DD...', '.DDDDD.', 'DDDDDDD', '..Z...R', '.ZZ..R.', '..Z....', '.Z.....'],\n  snow: ['N..N..N', '.......', '.N..N..', '.......', 'N..N..N', '.......', '.N..N..'],\n  sleet: ['..DD...', '.DDDDD.', 'DDDDDDD', '.DDDDD.', '.......', '.N.R.N.', 'R.N.R..'],\n  mist: ['FFFFF..', '.......', '..FFFFF', '.......', 'FFFFF..', '.......', '.FFFFFF'],\n  windy: ['WWWWW..', '.....W.', 'WWWWW..', '.......', 'WWWWWW.', '......W', 'WWWW...'],\n  heat: ['.SSS...', 'SSSSS..', '.SSS...', '.......', 'S.S.S.S', '.S.S.S.', '.......'],\n};\nconst miniLayer: Record<string, GlyphLayer> = { S: 'sun', M: 'moon', C: 'cloud', D: 'cloudDark', R: 'rain', Z: 'bolt', F: 'fog', N: 'snow', W: 'wind' };\n\nconst isDay = (hour: number) => {\n  const h = wrap(hour, 24);\n  return h >= SUNRISE && h <= SUNSET;\n};\n\nexport interface WeatherGlyphProps extends React.SVGAttributes<SVGSVGElement> {\n  kind: WeatherKind;\n  /** The hour it stands for: a clear or partly cloudy night draws the moon (default noon). */\n  hour?: number;\n}\n\n/** A 7 × 7 dot glyph for a kind of weather, in the weather recipe's inks. */\nexport function WeatherGlyph({ kind, hour = 12, className, ...props }: WeatherGlyphProps) {\n  const day = isDay(hour);\n  const key = kind === 'clear' ? (day ? 'sun' : 'moon') : kind === 'partly' && !day ? 'partlyNight' : kind;\n  const paths = Object.fromEntries(GLYPH_LAYERS.map((l) => [l, ''])) as Record<GlyphLayer, string>;\n  (MINI[key] ?? MINI.cloud).forEach((row, y) => [...row].forEach((c, x) => {\n    const l = miniLayer[c];\n    if (l) paths[l] += `M${x * 3 + 0.3} ${y * 3 + 0.3}h2.4v2.4h-2.4Z`;\n  }));\n  const own = 'mu-weather-glyph block flex-none';\n  return (\n    <svg aria-hidden shapeRendering=\"crispEdges\" viewBox=\"0 0 21 21\" data-kind={kind} className={className ? `${own} ${className}` : own} {...props}>\n      {GLYPH_LAYERS.map((l) => (paths[l] ? <path key={l} d={paths[l]} className={GLYPH_PAINT[l]} /> : null))}\n    </svg>\n  );\n}\n\n/* ── frames ─────────────────────────────────────────────── */\n\n/** A stepped frame counter; holds at 0 under reduced motion or when `on` is false. */\nfunction useFrames(on: boolean) {\n  const [tick, setTick] = React.useState(0);\n  React.useEffect(() => {\n    if (!on || typeof window === 'undefined') return;\n    const reduced = window.matchMedia('(prefers-reduced-motion: reduce)');\n    let id: number | undefined;\n    const run = () => {\n      window.clearInterval(id);\n      id = reduced.matches ? undefined : window.setInterval(() => setTick((t) => t + 1), FRAME_MS);\n    };\n    run();\n    reduced.addEventListener('change', run);\n    return () => { window.clearInterval(id); reduced.removeEventListener('change', run); };\n  }, [on]);\n  return on ? tick : 0;\n}\n\nconst deg = (n: number) => `${Math.round(n)}°`;\ntype Size = 'large' | 'tile';\nconst SizeContext = React.createContext<Size>('large');\n\n/* ── the parts ──────────────────────────────────────────── */\n\nconst ROOT: Record<Size, string> = {\n  large: 'mu-weather flex flex-col w-weather-width h-weather-height p-weather-pad gap-weather-gap',\n  tile: 'mu-weather-tile size-weather-tile-size p-weather-tile-pad',\n};\n\nexport interface WeatherRootProps extends React.HTMLAttributes<HTMLElement> {\n  /** large: the full widget (400 × 560). tile: the small square (180). */\n  size?: Size;\n}\n\n/** The slab: a raise surface at the card radius. */\nconst Root = React.forwardRef<HTMLElement, WeatherRootProps>(function WeatherRoot({ size = 'large', className, ...props }, ref) {\n  return (\n    <SizeContext.Provider value={size}>\n      <Surface ref={ref} as=\"section\" material=\"raise\" radius=\"card\" data-size={size} className={className ? `${ROOT[size]} ${className}` : ROOT[size]} {...props} />\n    </SizeContext.Provider>\n  );\n});\n\nexport interface WeatherHeaderProps extends Omit<React.HTMLAttributes<HTMLElement>, 'children'> {\n  place: string;\n  summary?: string;\n  /** The LED beside \"Live\" (default true); false shows \"Paused\" on an idle LED. Null hides the status. */\n  live?: boolean | null;\n  /** The clock beside the status: \"09:00\". */\n  clock?: string;\n}\n\n/** The place, one line of summary, and the status. */\nfunction Header({ place, summary, live = true, clock, className, ...props }: WeatherHeaderProps) {\n  const own = 'flex items-start justify-between gap-weather-gap px-weather-header-inset pt-weather-header-top';\n  return (\n    <header className={className ? `${own} ${className}` : own} {...props}>\n      <div className=\"flex flex-col gap-weather-header-gap\">\n        <h2 className=\"m-0 type-display text-ink\">{place}</h2>\n        {summary && <p className=\"m-0 type-meta text-ink2\">{summary}</p>}\n      </div>\n      {live !== null && (\n        <div className=\"flex items-center gap-weather-header-led-gap h-weather-header-height\">\n          <Led kind={live ? 'live' : 'off'} />\n          <span className=\"type-meta text-ink2\">{live ? 'Live' : 'Paused'}</span>\n          {clock && <span className=\"type-readout text-ink2\">{clock}</span>}\n        </div>\n      )}\n    </header>\n  );\n}\n\nconst SKY: Record<Size, string> = {\n  large: 'mu-weather-sky overflow-hidden flex-none h-weather-screen-height rounded-weather-screen-radius',\n  tile: 'mu-weather-sky overflow-hidden h-weather-tile-screen rounded-weather-screen-radius',\n};\nconst GRID: Record<Size, { cols: number; rows: number; horizon: number }> = {\n  large: { cols: 46, rows: 28, horizon: 21 },\n  tile: { cols: 21, rows: 21, horizon: 13 },\n};\n\nexport interface WeatherSkyProps extends React.HTMLAttributes<HTMLElement> {\n  /** The time of day in hours (9.5 is 09:30). Moves the sun and the moon. */\n  hour: number;\n  /** A kind (drawn from WEATHER_SKIES) or a sky of your own. */\n  sky: WeatherKind | WeatherSky;\n  /** The day, for the moon's phase when the sky names none. */\n  date?: Date;\n  /** Step the frames (default true). Reduced motion holds one frame regardless. */\n  animate?: boolean;\n  /** How the sky reads to a screen reader: \"Lisbon: sunny, 19°\". Without it the sky is decoration. */\n  label?: string;\n}\n\n/** The well with its dot-matrix scene; children (Weather.Now) sit on its ground. */\nconst Sky = React.forwardRef<HTMLElement, WeatherSkyProps>(function WeatherSkyPart({ hour, sky, date, animate = true, label, className, children, ...props }, ref) {\n  const size = React.useContext(SizeContext);\n  const tick = useFrames(animate);\n  const base = typeof sky === 'string' ? WEATHER_SKIES[sky] : sky;\n  const described: WeatherSky = base.moonPhase === undefined && date ? { ...base, moonPhase: moonAge(date) } : base;\n  const g = GRID[size];\n  const paths = weatherScene({ ...g, hour, sky: described, tick });\n  return (\n    <Well ref={ref} variant=\"field\" role={label ? 'img' : undefined} aria-label={label} data-kind={typeof sky === 'string' ? sky : 'custom'} className={className ? `${SKY[size]} ${className}` : SKY[size]} {...props}>\n      <svg aria-hidden shapeRendering=\"crispEdges\" viewBox={`0 0 ${g.cols * PITCH} ${g.rows * PITCH}`} className=\"absolute inset-0 block size-full\">\n        {WEATHER_SKY_LAYERS.map((l) => (paths[l] ? <path key={l} data-layer={l} d={paths[l]} className={SKY_PAINT[l]} /> : null))}\n      </svg>\n      {children}\n    </Well>\n  );\n});\n\nconst NOW: Record<Size, string> = {\n  large: 'absolute inset-x-weather-screen-inset bottom-weather-screen-foot flex items-end justify-between gap-weather-gap',\n  tile: 'absolute inset-x-weather-tile-inset bottom-weather-screen-foot flex items-end justify-between',\n};\n\nexport interface WeatherNowProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {\n  /** The temperature, in the unit you show. */\n  temp: number;\n  /** The condition in words: \"Sunny\". On a tile it is set engraved. */\n  condition: string;\n  /** One line under it: \"Feels 18° · Rain 0% · 9 km/h\". */\n  readout?: string;\n}\n\n/** The temperature in the pixel face on the sky's ground, the condition and one readout beside it. */\nfunction Now({ temp, condition, readout, className, ...props }: WeatherNowProps) {\n  const size = React.useContext(SizeContext);\n  return (\n    <div className={className ? `${NOW[size]} ${className}` : NOW[size]} {...props}>\n      <span className=\"type-pixel text-ink\">{deg(temp)}</span>\n      <div className={`flex flex-col items-end gap-weather-header-gap ${size === 'tile' ? 'pb-weather-tile-lift' : 'pb-weather-screen-lift'}`}>\n        <span className={size === 'tile' ? 'type-label engraved' : 'type-content text-ink'}>{condition}</span>\n        {readout && <span className=\"type-readout text-ink2\">{readout}</span>}\n      </div>\n    </div>\n  );\n}\n\nexport interface WeatherHoursProps extends Omit<React.OlHTMLAttributes<HTMLOListElement>, 'children'> {\n  /** Up to six slots, now first. */\n  hours: WeatherHour[];\n}\n\n/** The next hours: a label, a dot glyph and the temperature in the pixel face per slot. */\nfunction Hours({ hours, className, ...props }: WeatherHoursProps) {\n  const own = 'grid grid-cols-6 m-0 p-0 list-none h-weather-hour-height';\n  return (\n    <ol aria-label=\"Next hours\" className={className ? `${own} ${className}` : own} {...props}>\n      {hours.slice(0, 6).map((h, i) => (\n        <li key={`${h.label}-${i}`} aria-label={`${h.label}, ${deg(h.temp)}`} className=\"flex flex-col items-center justify-center gap-weather-hour-gap\">\n          <span className=\"type-label engraved\">{h.label}</span>\n          <WeatherGlyph kind={h.kind} hour={h.hour} className=\"size-weather-hour-glyph\" />\n          <span className=\"type-weather-hour text-ink\">{deg(h.temp)}</span>\n        </li>\n      ))}\n    </ol>\n  );\n}\n\nconst DAY_PAINT: Record<WeatherKind, string> = {\n  clear: 'fill-weather-ink-sun',\n  partly: 'fill-weather-ink-sun',\n  heat: 'fill-weather-ink-sun',\n  cloud: 'fill-weather-ink-cloud-dark',\n  mist: 'fill-weather-ink-cloud-dark',\n  windy: 'fill-weather-ink-cloud-dark',\n  drizzle: 'fill-weather-ink-rain',\n  rain: 'fill-weather-ink-rain',\n  storm: 'fill-weather-ink-rain',\n  sleet: 'fill-weather-ink-snow',\n  snow: 'fill-weather-ink-snow',\n};\n\nexport interface WeatherWeekProps extends Omit<React.HTMLAttributes<HTMLUListElement>, 'children'> {\n  /** Up to seven days, today first. */\n  days: WeatherDay[];\n  /** The shared scale, one dot a degree, in the unit you show (default 10–30; at most 21 degrees fit). */\n  scale?: [number, number];\n  /** The temperature now: a dark dot on today's row. */\n  now?: number;\n}\n\n/** The week: each day's low to high as lit dots on one shared scale, coloured by that day's weather. */\nfunction Week({ days, scale = [10, 30], now, className, ...props }: WeatherWeekProps) {\n  const [lo, hi] = scale;\n  const span = Math.min(20, Math.max(1, Math.round(hi - lo)));\n  const dot = (t: number) => `M${(Math.round(t) - lo) * PITCH + 1} 0h${DOT}v${DOT}h-${DOT}Z`;\n  const own = 'flex flex-col m-0 p-0 list-none gap-weather-week-gap px-weather-header-inset';\n  return (\n    <ul aria-label=\"The week\" className={className ? `${own} ${className}` : own} {...props}>\n      <li aria-hidden className=\"flex items-center gap-weather-week-col h-weather-week-row\">\n        <span className=\"w-weather-week-day type-label engraved\">{days.length} days</span>\n        <span className=\"size-weather-week-glyph\" />\n        <span className=\"w-weather-week-value text-right type-label engraved\">Low</span>\n        <span className=\"flex justify-between w-weather-week-bar type-readout text-ink2\">\n          <span>{deg(lo)}</span>\n          <span>{deg(lo + span / 2)}</span>\n          <span>{deg(lo + span)}</span>\n        </span>\n        <span className=\"w-weather-week-value type-label engraved\">High</span>\n      </li>\n      {days.slice(0, 7).map((d, i) => {\n        let off = '';\n        let lit = '';\n        for (let t = lo; t <= lo + span; t++) {\n          if (t >= d.low && t <= d.high) lit += dot(t);\n          else off += dot(t);\n        }\n        const today = i === 0;\n        return (\n          <li key={`${d.name}-${i}`} aria-label={`${d.name}: low ${deg(d.low)}, high ${deg(d.high)}`} className=\"flex items-center gap-weather-week-col h-weather-week-row\">\n            <span className={`w-weather-week-day ${today ? 'type-title' : 'type-ui'} text-ink`}>{d.name}</span>\n            <WeatherGlyph kind={d.kind} className=\"size-weather-week-glyph\" />\n            <span className=\"w-weather-week-value text-right type-ui text-ink2\">{deg(d.low)}</span>\n            <svg aria-hidden shapeRendering=\"crispEdges\" viewBox={`0 0 ${(span + 1) * PITCH} ${DOT}`} className=\"block flex-none w-weather-week-bar h-weather-week-bar-height\">\n              <path d={off} className=\"fill-weather-ink-off\" />\n              <path data-part=\"range\" d={lit} className={DAY_PAINT[d.kind]} />\n              {today && now !== undefined && <path data-part=\"now\" d={dot(now)} className=\"fill-ink\" />}\n            </svg>\n            <span className=\"w-weather-week-value type-title text-ink\">{deg(d.high)}</span>\n          </li>\n        );\n      })}\n    </ul>\n  );\n}\n\n/* ── the widgets, composed ──────────────────────────────── */\n\nexport interface WeatherProps extends Omit<React.HTMLAttributes<HTMLElement>, 'children'> {\n  place: string;\n  summary?: string;\n  /** The time of day in hours (9.5 is 09:30). Moves the sun and the moon. */\n  hour: number;\n  /** A kind, or a sky of your own (see WeatherSky). */\n  sky: WeatherKind | WeatherSky;\n  /** The condition in words: \"Sunny\". */\n  condition: string;\n  /** The temperature now, in the unit you show. */\n  temp: number;\n  feels?: number;\n  /** The chance of rain, 0–100. */\n  rain?: number;\n  /** The wind in words: \"9 km/h\". */\n  wind?: string;\n  live?: boolean | null;\n  clock?: string;\n  /** The day, for the moon's phase. */\n  date?: Date;\n  hours?: WeatherHour[];\n  days?: WeatherDay[];\n  scale?: [number, number];\n  animate?: boolean;\n}\n\n/** The large weather widget: Weather.Root with a header, the sky, the next hours and the week. */\nconst WeatherWidget = React.forwardRef<HTMLElement, WeatherProps>(function Weather(\n  { place, summary, hour, sky, condition, temp, feels, rain, wind, live, clock, date, hours = [], days = [], scale, animate, ...props },\n  ref,\n) {\n  const readout = [feels !== undefined ? `Feels ${deg(feels)}` : null, rain !== undefined ? `Rain ${Math.round(rain)}%` : null, wind ?? null].filter(Boolean).join(' · ');\n  return (\n    <Root ref={ref} aria-label={`Weather in ${place}`} {...props}>\n      <Header place={place} summary={summary} live={live} clock={clock} />\n      <Sky hour={hour} sky={sky} date={date} animate={animate} label={`${place}: ${condition}, ${deg(temp)}`}>\n        <Now temp={temp} condition={condition} readout={readout || undefined} />\n      </Sky>\n      {hours.length > 0 && <Hours hours={hours} />}\n      {days.length > 0 && (\n        <>\n          <Rule orientation=\"horizontal\" className=\"mx-weather-header-inset\" />\n          <Week days={days} scale={scale} now={temp} />\n        </>\n      )}\n    </Root>\n  );\n});\n\nexport const Weather = Object.assign(WeatherWidget, { Root, Header, Sky, Now, Hours, Week, Glyph: WeatherGlyph });\n\nexport interface WeatherTileProps extends Omit<React.HTMLAttributes<HTMLElement>, 'children'> {\n  /** A kind, or a sky of your own. */\n  sky: WeatherKind | WeatherSky;\n  /** The time of day in hours; moves the sun and the moon. */\n  hour: number;\n  /** The temperature, in the unit you show. */\n  temp: number;\n  /** The word on the ground: \"Rain\". */\n  name: string;\n  /** One readout under it: \"Rain 80%\". */\n  meta?: string;\n  date?: Date;\n  animate?: boolean;\n}\n\n/** A small weather tile: Weather.Root(tile) › Weather.Sky › Weather.Now. */\nexport const WeatherTile = React.forwardRef<HTMLElement, WeatherTileProps>(function WeatherTile(\n  { sky, hour, temp, name, meta, date, animate, ...props },\n  ref,\n) {\n  return (\n    <Root ref={ref} size=\"tile\" role=\"img\" aria-label={`${name}, ${deg(temp)}${meta ? `, ${meta}` : ''}`} {...props}>\n      <Sky hour={hour} sky={sky} date={date} animate={animate}>\n        <Now temp={temp} condition={name} readout={meta} />\n      </Sky>\n    </Root>\n  );\n});\n"
    }
  ],
  "docs": "Agent guide: https://metalui.dev/r/weather.md. SwiftUI: MetalWeather, MetalWeatherTile in the MetalUI Swift package."
}
