{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "moon-forecast",
  "type": "registry:component",
  "description": "Lunar phase card with local cycle estimates.",
  "registryDependencies": [
    "card",
    "button"
  ],
  "dependencies": [
    "lucide-react",
    "astronomy-engine@2.1.19"
  ],
  "files": [
    {
      "path": "packages/react/src/components/wxcn/moon-forecast.tsx",
      "type": "registry:component",
      "target": "@components/wxcn/moon-forecast.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\nimport type { IconSet } from './forecast-icons';\n\nimport { forecastDays, type ForecastDay } from '@/lib/wxcn/forecast-days.js';\nimport type { ForecastType, LocationInput, MoonForecast as MoonData } from '@/lib/wxcn/types.js';\nimport { getMoonForecast, getUpcomingMoonPhases, sampleMoon } from '@/lib/wxcn/moon.js';\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';\nimport { cn } from '@/lib/utils';\nimport { ForecastScreens } from './forecast-screens';\nimport MoonDisc from './moon-disc';\n\nexport type MoonForecastProps = {\n\ticonType?: IconSet;\n\tinteractive?: boolean;\n\ttimeZone?: string;\n\ttype?: ForecastType;\n\tsize?: 'sm' | 'default' | 'lg';\n\tdensity?: 'compact' | 'comfortable';\n\tclassName?: string;\n\tlocation?: LocationInput;\n\tforecast?: MoonData;\n\tsourceLabel?: string;\n};\n\nconst defaultLocation: LocationInput = {\n\tlabel: 'Austin, TX',\n\tlatitude: 30.2672,\n\tlongitude: -97.7431\n};\n\nfunction MoonCardView({\n\tday,\n\taction,\n\tforecast,\n\tdate,\n\ttype,\n\tsize,\n\tdensity,\n\tsourceLabel,\n\tlocation\n}: {\n\tday?: ForecastDay;\n\taction?: React.ReactNode;\n\tforecast: MoonData;\n\tdate: (value: string) => string;\n\ttype: ForecastType;\n\tsize: 'sm' | 'default' | 'lg';\n\tdensity: 'compact' | 'comfortable';\n\tsourceLabel: string;\n\tlocation: LocationInput;\n}) {\n\tconst view =\n\t\tday && day.entries[0].time !== Date.parse(forecast.date)\n\t\t\t? getMoonForecast(new Date(day.entries[0].time))\n\t\t\t: forecast;\n\treturn (\n\t\t<>\n\t\t\t<CardHeader>\n\t\t\t\t<CardTitle>{day?.label ?? 'Moon phase'}</CardTitle>\n\t\t\t\t<CardDescription>{location.label}</CardDescription>\n\t\t\t\t{action}\n\t\t\t</CardHeader>\n\t\t\t<CardContent\n\t\t\t\tclassName={cn(\n\t\t\t\t\t'grid px-[var(--card-spacing,var(--wxcn-spacing))]',\n\t\t\t\t\tdensity === 'compact' ? 'gap-3' : 'gap-5'\n\t\t\t\t)}\n\t\t\t>\n\t\t\t\t<div\n\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t'flex items-center gap-4',\n\t\t\t\t\t\tsize === 'lg' && 'flex-col rounded-lg bg-muted/20 p-5 text-center'\n\t\t\t\t\t)}\n\t\t\t\t>\n\t\t\t\t\t<MoonDisc\n\t\t\t\t\t\tphase={view.phase ?? view.age / 29.530588853}\n\t\t\t\t\t\tlabel={`${view.phaseName}, ${view.illumination}% illuminated`}\n\t\t\t\t\t\tclassName={\n\t\t\t\t\t\t\tsize === 'sm'\n\t\t\t\t\t\t\t\t? 'size-16 shrink-0'\n\t\t\t\t\t\t\t\t: size === 'lg'\n\t\t\t\t\t\t\t\t\t? 'size-36 shrink-0'\n\t\t\t\t\t\t\t\t\t: 'size-24 max-w-[28cqw] shrink-0'\n\t\t\t\t\t\t}\n\t\t\t\t\t/>\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<p\n\t\t\t\t\t\t\tstyle={{ fontSize: 'clamp(1rem,6cqw,1.5rem)' }}\n\t\t\t\t\t\t\tclassName={cn('font-medium tracking-tight', size === 'sm' ? 'text-lg' : 'text-2xl')}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{view.phaseName}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t\t<p className=\"mt-2 text-sm text-muted-foreground\">{view.illumination}% illuminated</p>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t{type !== 'simple' && (\n\t\t\t\t\t<dl className=\"moon-data divide-y text-sm\">\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t'flex justify-between gap-4 pb-3',\n\t\t\t\t\t\t\t\tdensity === 'compact' && 'py-[0.4rem]'\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<dt className=\"text-muted-foreground\">Moon age</dt>\n\t\t\t\t\t\t\t<dd>{view.age} days</dd>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t'flex justify-between gap-4 py-3',\n\t\t\t\t\t\t\t\tdensity === 'compact' && 'py-[0.4rem]'\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<dt className=\"text-muted-foreground\">Next full moon</dt>\n\t\t\t\t\t\t\t<dd>{date(view.nextFullMoon)}</dd>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t'flex justify-between gap-4 pt-3',\n\t\t\t\t\t\t\t\tdensity === 'compact' && 'py-[0.4rem]'\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<dt className=\"text-muted-foreground\">Next new moon</dt>\n\t\t\t\t\t\t\t<dd>{date(view.nextNewMoon)}</dd>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</dl>\n\t\t\t\t)}\n\t\t\t\t{!day && sourceLabel && (\n\t\t\t\t\t<p className=\"text-[10px] text-muted-foreground\">\n\t\t\t\t\t\t{sourceLabel} · {date(view.date)}\n\t\t\t\t\t</p>\n\t\t\t\t)}\n\t\t\t</CardContent>\n\t\t</>\n\t);\n}\n\nexport function MoonForecast({\n\ticonType,\n\tinteractive = false,\n\ttimeZone,\n\ttype = 'summary',\n\tsize = 'default',\n\tdensity = 'comfortable',\n\tclassName,\n\tlocation = defaultLocation,\n\tforecast = sampleMoon,\n\tsourceLabel = ''\n}: MoonForecastProps) {\n\tconst [visitorTimeZone, setVisitorTimeZone] = React.useState('UTC');\n\tReact.useEffect(() => setVisitorTimeZone(Intl.DateTimeFormat().resolvedOptions().timeZone), []);\n\tconst displayTimeZone = timeZone ?? location.timeZone ?? visitorTimeZone;\n\tconst date = React.useCallback(\n\t\t(value: string) =>\n\t\t\tnew Intl.DateTimeFormat('en-US', {\n\t\t\t\tmonth: 'short',\n\t\t\t\tday: 'numeric',\n\t\t\t\ttimeZone: displayTimeZone\n\t\t\t}).format(new Date(value)),\n\t\t[displayTimeZone]\n\t);\n\tconst phases = React.useMemo(\n\t\t() => getUpcomingMoonPhases(new Date(forecast.date)),\n\t\t[forecast.date]\n\t);\n\tconst days = React.useMemo(\n\t\t() =>\n\t\t\tforecastDays(\n\t\t\t\tphases.map((value) => ({\n\t\t\t\t\ttime: Date.parse(value.date),\n\t\t\t\t\tlabel: value.phaseName,\n\t\t\t\t\tsummary: `${value.illumination}% illuminated`,\n\t\t\t\t\tdetails: ''\n\t\t\t\t})),\n\t\t\t\tdisplayTimeZone\n\t\t\t),\n\t\t[displayTimeZone, phases]\n\t);\n\n\treturn (\n\t\t<Card\n\t\t\tstyle={\n\t\t\t\t{\n\t\t\t\t\tcontainerType: 'inline-size',\n\t\t\t\t\t['--wxcn-spacing' as string]: size === 'sm' ? '1rem' : '1.5rem'\n\t\t\t\t} as React.CSSProperties\n\t\t\t}\n\t\t\tdata-density={density}\n\t\t\tdata-card-size={size}\n\t\t\tdata-size={size === 'sm' ? 'sm' : 'default'}\n\t\t\tclassName={cn('relative isolate min-w-0 overflow-hidden', className)}\n\t\t>\n\t\t\t<ForecastScreens\n\t\t\t\tinteractive={interactive}\n\t\t\t\ticonType={iconType}\n\t\t\t\tdays={days}\n\t\t\t\ttitle=\"Moon\"\n\t\t\t\tdensity={density}\n\t\t\t\tsourceLabel={sourceLabel}\n\t\t\t\tactionLabel=\"Next phases\"\n\t\t\t\tsummaryTitle=\"Next phases\"\n\t\t\t\tsummary={(availableHeight) => (\n\t\t\t\t\t<div\n\t\t\t\t\t\tclassName=\"grid h-full auto-rows-[minmax(32px,1fr)] overflow-y-auto\"\n\t\t\t\t\t\tdata-slot=\"upcoming-moon-phases\"\n\t\t\t\t\t>\n\t\t\t\t\t\t{phases.map((phase) => (\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tkey={phase.date}\n\t\t\t\t\t\t\t\tclassName=\"flex min-h-0 items-center gap-3 border-b text-xs last:border-0\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\tclassName=\"shrink-0\"\n\t\t\t\t\t\t\t\t\tstyle={{ width: Math.max(16, Math.min(32, availableHeight / 7 - 4)) }}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<MoonDisc phase={phase.phase} label={phase.phaseName} className=\"size-full\" />\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<span className=\"min-w-0 flex-1 font-medium\">{phase.phaseName}</span>\n\t\t\t\t\t\t\t\t<time dateTime={phase.date} className=\"shrink-0 text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t{date(phase.date)}\n\t\t\t\t\t\t\t\t</time>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t))}\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\t\t\t\tdetail={(day, action) => (\n\t\t\t\t\t<MoonCardView\n\t\t\t\t\t\tday={day}\n\t\t\t\t\t\tforecast={forecast}\n\t\t\t\t\t\tdate={date}\n\t\t\t\t\t\ttype={type}\n\t\t\t\t\t\tsize={size}\n\t\t\t\t\t\tdensity={density}\n\t\t\t\t\t\tsourceLabel={sourceLabel}\n\t\t\t\t\t\tlocation={location}\n\t\t\t\t\t\taction={action(false)}\n\t\t\t\t\t/>\n\t\t\t\t)}\n\t\t\t>\n\t\t\t\t{(_openDay, action) => (\n\t\t\t\t\t<MoonCardView\n\t\t\t\t\t\tforecast={forecast}\n\t\t\t\t\t\tdate={date}\n\t\t\t\t\t\ttype={type}\n\t\t\t\t\t\tsize={size}\n\t\t\t\t\t\tdensity={density}\n\t\t\t\t\t\tsourceLabel={sourceLabel}\n\t\t\t\t\t\tlocation={location}\n\t\t\t\t\t\taction={action(false)}\n\t\t\t\t\t/>\n\t\t\t\t)}\n\t\t\t</ForecastScreens>\n\t\t</Card>\n\t);\n}\n\nexport default MoonForecast;\n"
    },
    {
      "path": "packages/react/src/components/wxcn/forecast-screens.tsx",
      "type": "registry:component",
      "target": "@components/wxcn/forecast-screens.tsx",
      "content": "'use client';\n\nimport { useEffect, useLayoutEffect, useRef, useState, type ReactNode } from 'react';\nimport { CardAction, CardContent, CardHeader, CardTitle } from '@/components/ui/card';\nimport { Button } from '@/components/ui/button';\nimport { ForecastIcon, type IconSet } from './forecast-icons';\nimport type { ForecastDay } from '@/lib/wxcn/forecast-days.js';\nexport type ForecastAction = (onSurface: boolean) => ReactNode;\nexport type OpenForecastDay = (time: string, trigger: HTMLElement) => void;\nexport interface ForecastScreensProps {\n\tinteractive: boolean;\n\tdays: ForecastDay[];\n\ttitle: string;\n\tdensity: string;\n\tsourceLabel: string;\n\tsummary?: (availableHeight: number) => ReactNode;\n\tdaySummary?: (day: ForecastDay) => ReactNode;\n\tactionLabel?: string;\n\tsummaryTitle?: string;\n\tshowWeek?: boolean;\n\ticonType?: IconSet;\n\tflush?: boolean;\n\tdetail: (day: ForecastDay, action: ForecastAction) => ReactNode;\n\tchildren: (\n\t\topenDay: OpenForecastDay,\n\t\taction: ForecastAction,\n\t\toverviewVisible: boolean\n\t) => ReactNode;\n}\nexport function ForecastScreens({\n\tinteractive,\n\tdays,\n\ttitle,\n\tsummary,\n\tdaySummary,\n\tactionLabel = 'View week',\n\tsummaryTitle = 'Upcoming tides',\n\tshowWeek = true,\n\ticonType,\n\tflush = false,\n\tdetail,\n\tchildren\n}: ForecastScreensProps) {\n\tconst [screen, setScreen] = useState<'overview' | 'week' | 'day'>('overview');\n\tconst [selectedKey, setSelectedKey] = useState('');\n\tconst [availableHeight, setAvailableHeight] = useState(0);\n\tconst surface = useRef<HTMLDivElement>(null);\n\tconst table = useRef<HTMLDivElement>(null);\n\tconst host = useRef<HTMLElement | null>(null);\n\tconst fromWeek = useRef(false);\n\tconst originIndex = useRef(0);\n\tconst originTrigger = useRef<HTMLElement | null>(null);\n\tconst focusTarget = useRef<'surface' | 'origin' | 'day' | null>(null);\n\tconst selected = days.find((day) => day.key === selectedKey);\n\tconst active = interactive && (screen !== 'day' || selected) ? screen : 'overview';\n\tuseEffect(() => {\n\t\tif (active !== screen) setScreen('overview');\n\t}, [active, screen]);\n\tuseLayoutEffect(() => {\n\t\tif (active !== 'week' || !table.current) return;\n\t\tconst node = table.current;\n\t\tconst update = () => setAvailableHeight(node.clientHeight);\n\t\tconst observer = new ResizeObserver(update);\n\t\tobserver.observe(node);\n\t\tupdate();\n\t\treturn () => observer.disconnect();\n\t}, [active]);\n\tuseLayoutEffect(() => {\n\t\tconst target = focusTarget.current;\n\t\tif (!target) return;\n\t\tif (target === 'surface') {\n\t\t\tsurface.current?.focus({ preventScroll: true });\n\t\t\tfocusTarget.current = null;\n\t\t\treturn;\n\t\t}\n\t\tif ((target === 'day' && active !== 'week') || (target === 'origin' && active !== 'overview'))\n\t\t\treturn;\n\t\t// Wait for the retained overview or resized detail surface to become visible.\n\t\tlet frame = requestAnimationFrame(() => {\n\t\t\tframe = requestAnimationFrame(() => {\n\t\t\t\tconst trigger =\n\t\t\t\t\ttarget === 'day'\n\t\t\t\t\t\t? host.current?.querySelector<HTMLButtonElement>(`[data-forecast-day=\"${selectedKey}\"]`)\n\t\t\t\t\t\t: originTrigger.current?.isConnected\n\t\t\t\t\t\t\t? originTrigger.current\n\t\t\t\t\t\t\t: host.current?.querySelectorAll<HTMLButtonElement>('button')[originIndex.current];\n\t\t\t\ttrigger?.focus({ preventScroll: true });\n\t\t\t\tif (document.activeElement === trigger) focusTarget.current = null;\n\t\t\t});\n\t\t});\n\t\treturn () => cancelAnimationFrame(frame);\n\t}, [active, selectedKey, availableHeight]);\n\n\tfunction open(next: 'week' | 'day', trigger: HTMLElement, key = '') {\n\t\tif (active === 'overview') {\n\t\t\thost.current = trigger.closest<HTMLElement>('[data-slot=card]');\n\t\t\toriginTrigger.current = trigger;\n\t\t\toriginIndex.current = host.current\n\t\t\t\t? [...host.current.querySelectorAll('button')].indexOf(trigger as HTMLButtonElement)\n\t\t\t\t: 0;\n\t\t}\n\t\tfromWeek.current = active === 'week';\n\t\tsetSelectedKey(key);\n\t\tfocusTarget.current = 'surface';\n\t\tsetScreen(next);\n\t}\n\tconst openDay: OpenForecastDay = (time, trigger) => {\n\t\tconst day = days.find((day) => day.entries.some((entry) => entry.time === Date.parse(time)));\n\t\tif (day) open('day', trigger, day.key);\n\t};\n\tfunction back() {\n\t\tif (active === 'day' && fromWeek.current) {\n\t\t\tfocusTarget.current = 'day';\n\t\t\tsetScreen('week');\n\t\t} else {\n\t\t\tfocusTarget.current = 'origin';\n\t\t\tsetScreen('overview');\n\t\t}\n\t}\n\tconst weekAction: ForecastAction = (onSurface) =>\n\t\tinteractive && showWeek ? (\n\t\t\t<CardAction>\n\t\t\t\t<Button\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\tclassName={`h-6 border border-transparent px-1.5 text-[10px] font-medium ${onSurface ? 'text-white/80 hover:bg-white/10 hover:text-white' : 'text-muted-foreground hover:text-foreground'}`}\n\t\t\t\t\taria-label={summary ? actionLabel : `View ${title.toLowerCase()} week`}\n\t\t\t\t\tonClick={(event) => open('week', event.currentTarget)}\n\t\t\t\t>\n\t\t\t\t\t{actionLabel}\n\t\t\t\t</Button>\n\t\t\t</CardAction>\n\t\t) : null;\n\tconst backAction: ForecastAction = (onSurface) => (\n\t\t<CardAction>\n\t\t\t<Button\n\t\t\t\ttype=\"button\"\n\t\t\t\tvariant=\"ghost\"\n\t\t\t\tsize=\"sm\"\n\t\t\t\tclassName={`h-6 gap-1 border border-transparent px-1.5 text-[10px] ${onSurface ? 'text-white/80 hover:bg-white/10 hover:text-white' : 'text-muted-foreground'}`}\n\t\t\t\tonClick={back}\n\t\t\t>\n\t\t\t\t<ForecastIcon name=\"arrowDown\" iconSet={iconType} className=\"size-3 rotate-90\" />\n\t\t\t\tBack\n\t\t\t</Button>\n\t\t</CardAction>\n\t);\n\treturn (\n\t\t<>\n\t\t\t<div\n\t\t\t\tclassName=\"contents\"\n\t\t\t\tstyle={{ visibility: active === 'overview' ? 'visible' : 'hidden' }}\n\t\t\t\tinert={active !== 'overview'}\n\t\t\t\taria-hidden={active !== 'overview'}\n\t\t\t>\n\t\t\t\t{children(openDay, weekAction, active === 'overview')}\n\t\t\t</div>\n\t\t\t{active !== 'overview' && (\n\t\t\t\t<div\n\t\t\t\t\tref={surface}\n\t\t\t\t\ttabIndex={-1}\n\t\t\t\t\trole=\"group\"\n\t\t\t\t\taria-label={\n\t\t\t\t\t\tactive === 'day'\n\t\t\t\t\t\t\t? `${selected?.label} ${title.toLowerCase()} forecast`\n\t\t\t\t\t\t\t: summary\n\t\t\t\t\t\t\t\t? summaryTitle\n\t\t\t\t\t\t\t\t: `${title} • Week`\n\t\t\t\t\t}\n\t\t\t\t\tdata-slot=\"forecast-screen\"\n\t\t\t\t\tonKeyDown={(event) => {\n\t\t\t\t\t\tif (event.key === 'Escape') {\n\t\t\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\t\t\tback();\n\t\t\t\t\t\t}\n\t\t\t\t\t}}\n\t\t\t\t\tclassName={`absolute inset-0 z-10 flex min-h-0 flex-col overflow-hidden rounded-[inherit] bg-card text-card-foreground outline-none ${active === 'day' && flush ? 'gap-0' : active === 'week' ? 'gap-2 py-3' : 'gap-[var(--card-spacing,var(--wxcn-spacing,1.5rem))] py-[var(--card-spacing,var(--wxcn-spacing,1.5rem))]'}`}\n\t\t\t\t>\n\t\t\t\t\t{active === 'day' && selected ? (\n\t\t\t\t\t\tdetail(selected, backAction)\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t<CardHeader className=\"shrink-0\">\n\t\t\t\t\t\t\t\t<CardTitle className=\"truncate\">\n\t\t\t\t\t\t\t\t\t{summary ? summaryTitle : `${title} • Week`}\n\t\t\t\t\t\t\t\t</CardTitle>\n\t\t\t\t\t\t\t\t{backAction(false)}\n\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t<CardContent className=\"min-h-0 min-w-0 flex-1\">\n\t\t\t\t\t\t\t\t<div ref={table} className=\"h-full min-h-0\" data-slot=\"forecast-week-table\">\n\t\t\t\t\t\t\t\t\t{summary ? (\n\t\t\t\t\t\t\t\t\t\tsummary(availableHeight)\n\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\t\t\tclassName={`grid h-full content-start gap-x-3 overflow-y-auto ${!daySummary && availableHeight < days.length * 28 ? 'grid-cols-2' : 'grid-cols-1'}`}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t{days.map((day) => (\n\t\t\t\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\t\t\t\tkey={day.key}\n\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tdata-forecast-day={day.key}\n\t\t\t\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\theight: daySummary\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? Math.max(32, availableHeight / days.length)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: Math.min(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t28,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tavailableHeight /\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t(!daySummary && availableHeight < days.length * 28\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? Math.ceil(days.length / 2)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: days.length)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tfontSize: daySummary\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? `clamp(12px, min(4.5cqw, ${availableHeight / days.length / 3}px), 20px)`\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: undefined\n\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"flex min-h-0 min-w-0 items-center justify-between gap-2 border-b text-left text-[11px] hover:bg-muted/50 focus-visible:outline-2 focus-visible:outline-ring\"\n\t\t\t\t\t\t\t\t\t\t\t\t\taria-label={`View details for ${day.label}`}\n\t\t\t\t\t\t\t\t\t\t\t\t\tonClick={(event) => open('day', event.currentTarget, day.key)}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{daySummary ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdaySummary(day)\n\t\t\t\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"shrink-0 font-medium\">{day.label}</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"truncate text-muted-foreground\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttitle={day.entries.map((entry) => entry.summary).join(' · ')}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{day.entries.map((entry) => entry.summary).join(' · ')}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t</>\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t)}\n\t\t</>\n\t);\n}\n"
    },
    {
      "path": "packages/react/src/components/wxcn/moon-disc.tsx",
      "type": "registry:component",
      "target": "@components/wxcn/moon-disc.tsx",
      "content": "import * as React from 'react';\n\nimport { cn } from '@/lib/utils';\n\nexport type MoonDiscProps = React.SVGProps<SVGSVGElement> & {\n\tphase?: number;\n\tlabel?: string;\n};\n\nexport function MoonDisc({\n\tphase = 0.5,\n\tlabel = 'Full moon',\n\tclassName = 'size-24',\n\t...props\n}: MoonDiscProps) {\n\tconst id = React.useId().replace(/:/g, '');\n\tconst p = ((phase % 1) + 1) % 1;\n\tconst waxing = p <= 0.5;\n\tconst sign = waxing ? 1 : -1;\n\tconst terminator = Math.cos(p * Math.PI * 2) * sign;\n\tconst points = Array.from({ length: 97 }, (_, i) => {\n\t\tconst y = 1 - i / 48;\n\t\treturn `${50 + 46 * Math.sqrt(Math.max(0, 1 - y * y)) * terminator} ${50 + 46 * y}`;\n\t});\n\tconst light = `M50 4 A46 46 0 0 ${waxing ? 1 : 0} 50 96 L${points.join(' L')} Z`;\n\n\treturn (\n\t\t<svg viewBox=\"0 0 100 100\" className={cn(className)} role=\"img\" aria-label={label} {...props}>\n\t\t\t<defs>\n\t\t\t\t<radialGradient id={`${id}-surface`} cx=\"42%\" cy=\"38%\" r=\"65%\">\n\t\t\t\t\t<stop stopColor=\"#f1f1ed\" />\n\t\t\t\t\t<stop offset=\".72\" stopColor=\"#d9dad5\" />\n\t\t\t\t\t<stop offset=\"1\" stopColor=\"#a8aca8\" />\n\t\t\t\t</radialGradient>\n\t\t\t</defs>\n\t\t\t<circle cx=\"50\" cy=\"50\" r=\"46\" fill=\"#30343a\" />\n\t\t\t<path d={light} fill={`url(#${id}-surface)`} />\n\t\t\t<circle\n\t\t\t\tcx=\"50\"\n\t\t\t\tcy=\"50\"\n\t\t\t\tr=\"46\"\n\t\t\t\tfill=\"none\"\n\t\t\t\tstroke=\"currentColor\"\n\t\t\t\tstrokeOpacity=\".08\"\n\t\t\t\tstrokeWidth=\".5\"\n\t\t\t/>\n\t\t</svg>\n\t);\n}\n\nexport default MoonDisc;\n"
    },
    {
      "path": "packages/react/src/icons/forecast-icons.tsx",
      "type": "registry:component",
      "target": "@components/wxcn/forecast-icons.tsx",
      "content": "import { IconPlaceholder } from '@/components/icon-placeholder';\nexport type IconName = 'arrowUp' | 'arrowDown' | 'weather' | 'sun' | 'moon' | 'tide' | 'wind' | 'rain' | 'snow';\nexport type IconSet = 'lucide' | 'tabler' | 'phosphor' | 'hugeicons' | 'remixicon';\nexport type ForecastIconProps = { name: IconName; iconSet?: IconSet; className?: string };\nexport function ForecastIcon({ name, className = 'size-5' }: ForecastIconProps) {\n switch (name) {\n case 'arrowUp': return <IconPlaceholder lucide=\"ArrowUpIcon\" tabler=\"IconArrowUp\" phosphor=\"ArrowUpIcon\" hugeicons=\"ArrowUp02Icon\" remixicon=\"RiArrowUpLine\" className={className} aria-hidden=\"true\" />;\ncase 'arrowDown': return <IconPlaceholder lucide=\"ArrowDownIcon\" tabler=\"IconArrowDown\" phosphor=\"ArrowDownIcon\" hugeicons=\"ArrowDown02Icon\" remixicon=\"RiArrowDownLine\" className={className} aria-hidden=\"true\" />;\ncase 'weather': return <IconPlaceholder lucide=\"CloudSunIcon\" tabler=\"IconCloud\" phosphor=\"CloudSunIcon\" hugeicons=\"SunCloud02Icon\" remixicon=\"RiCloudy2Line\" className={className} aria-hidden=\"true\" />;\ncase 'sun': return <IconPlaceholder lucide=\"SunIcon\" tabler=\"IconSun\" phosphor=\"SunIcon\" hugeicons=\"Sun03Icon\" remixicon=\"RiSunLine\" className={className} aria-hidden=\"true\" />;\ncase 'moon': return <IconPlaceholder lucide=\"MoonIcon\" tabler=\"IconMoon\" phosphor=\"MoonIcon\" hugeicons=\"Moon02Icon\" remixicon=\"RiMoonLine\" className={className} aria-hidden=\"true\" />;\ncase 'tide': return <IconPlaceholder lucide=\"WavesIcon\" tabler=\"IconWavesElectricity\" phosphor=\"WavesIcon\" hugeicons=\"WaterfallUp01Icon\" remixicon=\"RiWaterFlashLine\" className={className} aria-hidden=\"true\" />;\ncase 'wind': return <IconPlaceholder lucide=\"WindIcon\" tabler=\"IconWind\" phosphor=\"WindIcon\" hugeicons=\"WindPower02Icon\" remixicon=\"RiWindyLine\" className={className} aria-hidden=\"true\" />;\ncase 'rain': return <IconPlaceholder lucide=\"CloudRainIcon\" tabler=\"IconCloudRain\" phosphor=\"CloudRainIcon\" hugeicons=\"CloudRainIcon\" remixicon=\"RiRainyLine\" className={className} aria-hidden=\"true\" />;\ncase 'snow': return <IconPlaceholder lucide=\"SnowflakeIcon\" tabler=\"IconSnowflake\" phosphor=\"SnowflakeIcon\" hugeicons=\"SnowIcon\" remixicon=\"RiSnowyLine\" className={className} aria-hidden=\"true\" />;\n }\n}\n"
    },
    {
      "path": "packages/core/src/types.ts",
      "type": "registry:lib",
      "target": "@lib/wxcn/types.ts",
      "content": "export type CardSize = 'sm' | 'default' | 'lg';\nexport type CardDensity = 'compact' | 'comfortable';\nexport type ForecastType = 'summary' | 'detailed' | 'simple';\nexport type IconSet = 'hugeicons' | 'phosphor-svelte' | 'lucide' | 'tabler' | 'remix';\nexport type WeatherUnit = 'fahrenheit' | 'celsius';\nexport type TideUnit = 'ft' | 'meter';\n\nexport type LocationInput = {\n\tlabel?: string;\n\tlatitude: number;\n\tlongitude: number;\n\tstation?: string;\n\ttimeZone?: string;\n};\n\nexport type WeatherPeriod = {\n\tname: string;\n\tstartTime: string;\n\tendTime?: string;\n\ttemperature: number;\n\ttemperatureUnit: string;\n\twindSpeed: string;\n\twindDirection: string;\n\tshortForecast: string;\n\tdetailedForecast: string;\n\tisDaytime: boolean;\n};\n\nexport type TidePrediction = {\n\ttime: string;\n\theight: string;\n\ttype: 'H' | 'L';\n};\n\nexport type MoonForecast = {\n\tdate: string;\n\tphaseName: string;\n\t/** Astronomical phase cycle: 0 new, 0.25 first quarter, 0.5 full. */\n\tphase?: number;\n\tillumination: number;\n\tage: number;\n\tnextFullMoon: string;\n\tnextNewMoon: string;\n};\n\n/** Feet above MLLW. Prefer ISO 8601 timestamps with an explicit offset. */\nexport type TidePoint = { time: string; height: string };\nexport type TideReading = TidePoint;\n\n/** Current observed conditions; daily extrema use temperatureUnit and the location's calendar day. */\nexport type CurrentWeather = WeatherPeriod & {\n\tobservedAt: string;\n\thighToday?: number;\n\tlowToday?: number;\n};\n\nexport type WeatherBackground = 'none' | 'realistic' | 'dithered' | 'gradient';\n"
    },
    {
      "path": "packages/core/src/forecast-days.ts",
      "type": "registry:lib",
      "target": "@lib/wxcn/forecast-days.ts",
      "content": "export type ForecastEntry = { time: number; label: string; summary: string; details: string };\nexport type ForecastDay = { key: string; label: string; entries: ForecastEntry[] };\n\n/** Group supplied periods in the location's calendar, without inventing missing days. */\nexport function forecastDays(entries: ForecastEntry[], timeZone: string): ForecastDay[] {\n\tconst key = new Intl.DateTimeFormat('en-CA', {\n\t\ttimeZone,\n\t\tyear: 'numeric',\n\t\tmonth: '2-digit',\n\t\tday: '2-digit'\n\t});\n\tconst label = new Intl.DateTimeFormat('en-US', {\n\t\ttimeZone,\n\t\tweekday: 'short',\n\t\tmonth: 'short',\n\t\tday: 'numeric'\n\t});\n\tconst days = new Map<string, ForecastDay>();\n\tfor (const entry of entries\n\t\t.filter((e) => Number.isFinite(e.time))\n\t\t.toSorted((a, b) => a.time - b.time)) {\n\t\tconst id = key.format(entry.time);\n\t\tif (!days.has(id)) days.set(id, { key: id, label: label.format(entry.time), entries: [] });\n\t\tdays.get(id)!.entries.push(entry);\n\t}\n\treturn [...days.values()].slice(0, 7);\n}\n\n/** Resolve noon on a forecast calendar date in the location's time zone, including DST. */\nexport function forecastDayNoon(key: string, timeZone: string): number {\n\tconst noon = Date.parse(`${key}T12:00:00Z`);\n\tconst format = new Intl.DateTimeFormat('en-US', {\n\t\ttimeZone,\n\t\tyear: 'numeric',\n\t\tmonth: '2-digit',\n\t\tday: '2-digit',\n\t\thour: '2-digit',\n\t\tminute: '2-digit',\n\t\tsecond: '2-digit',\n\t\thourCycle: 'h23'\n\t});\n\tlet instant = noon;\n\tfor (let attempt = 0; attempt < 3; attempt++) {\n\t\tconst parts = Object.fromEntries(\n\t\t\tformat.formatToParts(instant).map(({ type, value }) => [type, value])\n\t\t);\n\t\tconst wallTime = Date.UTC(\n\t\t\tNumber(parts.year),\n\t\t\tNumber(parts.month) - 1,\n\t\t\tNumber(parts.day),\n\t\t\tNumber(parts.hour),\n\t\t\tNumber(parts.minute),\n\t\t\tNumber(parts.second)\n\t\t);\n\t\tconst correction = noon - wallTime;\n\t\tinstant += correction;\n\t\tif (!correction) break;\n\t}\n\treturn instant;\n}\n"
    },
    {
      "path": "packages/core/src/moon.ts",
      "type": "registry:lib",
      "target": "@lib/wxcn/moon.ts",
      "content": "import { Body, Illumination, MoonPhase, SearchMoonPhase } from 'astronomy-engine';\nimport type { MoonForecast } from './types.js';\n\nconst phaseNames = [\n\t'New Moon',\n\t'Waxing Crescent',\n\t'First Quarter',\n\t'Waxing Gibbous',\n\t'Full Moon',\n\t'Waning Gibbous',\n\t'Last Quarter',\n\t'Waning Crescent'\n];\n\nexport function getMoonForecast(date = new Date()): MoonForecast {\n\tconst phase = MoonPhase(date) / 360;\n\tconst previousNew = SearchMoonPhase(0, date, -40)!;\n\tconst nextNew = SearchMoonPhase(0, date, 40)!;\n\tconst nextFull = SearchMoonPhase(180, date, 40)!;\n\treturn {\n\t\tdate: date.toISOString(),\n\t\tphase,\n\t\tphaseName: phaseNames[Math.round(phase * 8) % 8],\n\t\tillumination: Math.round(Illumination(Body.Moon, date).phase_fraction * 100),\n\t\tage: Number(((date.getTime() - previousNew.date.getTime()) / 86400000).toFixed(1)),\n\t\tnextFullMoon: nextFull.date.toISOString(),\n\t\tnextNewMoon: nextNew.date.toISOString()\n\t};\n}\n\n/** The next seven named phases, at their exact 45-degree lunar phase crossings. */\nexport function getUpcomingMoonPhases(date = new Date()): MoonForecast[] {\n\treturn phaseNames\n\t\t.map((phaseName, index) => {\n\t\t\tconst event = SearchMoonPhase(index * 45, new Date(date.getTime() + 1000), 40)!;\n\t\t\treturn { ...getMoonForecast(event.date), phase: index / 8, phaseName };\n\t\t})\n\t\t.sort((a, b) => Date.parse(a.date) - Date.parse(b.date))\n\t\t.slice(0, 7);\n}\n\nexport const sampleMoon = getMoonForecast(new Date('2026-09-06T12:00:00-05:00'));\n"
    }
  ]
}
