{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "widgets",
  "title": "Widgets",
  "description": "Desktop widgets for weather, a calendar day and device batteries, in small, medium and large sizes that scale with their width.",
  "dependencies": [
    "lucide-react"
  ],
  "files": [
    {
      "path": "registry/tealmac/widgets.tsx",
      "content": "// Adapted from Ballmac UI (https://github.com/vamsiy78/ballmac-ui), MIT License, Copyright (c) 2026 Ballmac.\n\"use client\"\n\nimport * as React from \"react\"\nimport {\n  Cloud,\n  CloudLightning,\n  CloudRain,\n  CloudSnow,\n  CloudSun,\n  Moon,\n  Navigation,\n  Sun,\n  Zap,\n} from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\ntype WidgetSize = \"small\" | \"medium\" | \"large\"\n\n/** Small is a square, medium is twice as wide, large is twice as wide and as tall. Every size scales with its width. */\nconst SHAPE: Record<WidgetSize, string> = {\n  small: \"aspect-square\",\n  medium: \"aspect-[2.14/1]\",\n  large: \"aspect-[0.955/1]\",\n}\n\ntype WidgetProps = React.ComponentProps<\"section\"> & {\n  /** Which size the widget draws at. Set the width with `className` (a small widget is about 10rem wide). */\n  size?: WidgetSize\n  /** Accessible name of the widget. */\n  label: string\n  /** Class names for the widget's surface (background and text color). `className` sizes and places the widget. */\n  surfaceClassName?: string\n}\n\n/** Frame for a desktop widget: rounded, container-scaled, and one of three sizes. Use it for your own widgets. */\nfunction Widget({\n  size = \"small\",\n  label,\n  className,\n  surfaceClassName,\n  children,\n  ...props\n}: WidgetProps) {\n  return (\n    <section\n      data-slot=\"widget\"\n      data-size={size}\n      aria-label={label}\n      className={cn(\n        \"@container relative w-40 max-w-full\",\n        SHAPE[size],\n        size !== \"small\" && \"w-80\",\n        className\n      )}\n      {...props}\n    >\n      <div\n        data-slot=\"widget-surface\"\n        className={cn(\n          \"absolute inset-0 overflow-hidden rounded-[calc(var(--u)*14)] shadow-[0_8px_24px_-8px_rgb(0_0_0/0.35),0_0_0_1px_rgb(0_0_0/0.06)]\",\n          size === \"small\" ? \"[--u:1cqw]\" : \"[--u:0.467cqw]\",\n          surfaceClassName\n        )}\n      >\n        {children}\n      </div>\n    </section>\n  )\n}\n\n/** Font size in the widget's own unit. */\nconst fs = (n: number): React.CSSProperties => ({\n  fontSize: `calc(var(--u) * ${n})`,\n})\nconst sp = (n: number) => `calc(var(--u) * ${n})`\n\n// Weather\n\ntype WeatherCondition =\n  \"clear\" | \"partly-cloudy\" | \"cloudy\" | \"rain\" | \"snow\" | \"storm\" | \"night\"\n\nconst SKY: Record<WeatherCondition, string> = {\n  clear:\n    \"bg-[linear-gradient(165deg,oklch(0.47_0.17_255),oklch(0.58_0.13_232))]\",\n  \"partly-cloudy\":\n    \"bg-[linear-gradient(165deg,oklch(0.46_0.15_255),oklch(0.57_0.1_238))]\",\n  cloudy:\n    \"bg-[linear-gradient(165deg,oklch(0.44_0.04_255),oklch(0.55_0.035_245))]\",\n  rain: \"bg-[linear-gradient(165deg,oklch(0.36_0.06_260),oklch(0.48_0.06_245))]\",\n  snow: \"bg-[linear-gradient(165deg,oklch(0.46_0.06_245),oklch(0.57_0.045_235))]\",\n  storm:\n    \"bg-[linear-gradient(165deg,oklch(0.28_0.07_290),oklch(0.4_0.08_270))]\",\n  night:\n    \"bg-[linear-gradient(165deg,oklch(0.22_0.08_275),oklch(0.34_0.1_262))]\",\n}\n\nconst SKY_ICON: Record<\n  WeatherCondition,\n  React.ComponentType<{ className?: string }>\n> = {\n  clear: Sun,\n  \"partly-cloudy\": CloudSun,\n  cloudy: Cloud,\n  rain: CloudRain,\n  snow: CloudSnow,\n  storm: CloudLightning,\n  night: Moon,\n}\n\nconst SKY_WORD: Record<WeatherCondition, string> = {\n  clear: \"Clear\",\n  \"partly-cloudy\": \"Partly Cloudy\",\n  cloudy: \"Cloudy\",\n  rain: \"Rain\",\n  snow: \"Snow\",\n  storm: \"Thunderstorms\",\n  night: \"Clear Night\",\n}\n\ntype HourlyForecast = {\n  label: string\n  temp: number\n  condition: WeatherCondition\n}\ntype DailyForecast = {\n  day: string\n  low: number\n  high: number\n  condition: WeatherCondition\n}\n\ntype WeatherWidgetProps = Omit<\n  WidgetProps,\n  \"label\" | \"children\" | \"surfaceClassName\"\n> & {\n  /** Place name. */\n  city: string\n  /** Current temperature, in degrees. */\n  temperature: number\n  /** Current sky. Chooses the gradient, icon and wording. */\n  condition?: WeatherCondition\n  /** Today's high. */\n  high: number\n  /** Today's low. */\n  low: number\n  /** Upcoming hours, shown on medium and large widgets. */\n  hourly?: HourlyForecast[]\n  /** Upcoming days, shown on the large widget. */\n  daily?: DailyForecast[]\n}\n\nfunction SkyIcon({\n  condition,\n  className,\n  style,\n}: {\n  condition: WeatherCondition\n  className?: string\n  style?: React.CSSProperties\n}) {\n  const Icon = SKY_ICON[condition]\n  return (\n    <span\n      aria-hidden=\"true\"\n      className={cn(\n        \"flex items-center justify-center drop-shadow-[0_1px_3px_rgb(0_0_0/0.25)]\",\n        className\n      )}\n      style={style}\n    >\n      <Icon className=\"size-full\" />\n    </span>\n  )\n}\n\n/** A weather widget with a sky gradient that follows the condition, hourly strip and daily ranges. */\nfunction WeatherWidget({\n  city,\n  temperature,\n  condition = \"clear\",\n  high,\n  low,\n  hourly = [],\n  daily = [],\n  size = \"small\",\n  className,\n  ...props\n}: WeatherWidgetProps) {\n  const min = Math.min(...daily.map((d) => d.low), low)\n  const max = Math.max(...daily.map((d) => d.high), high)\n  const summary = `${city}, ${temperature} degrees, ${SKY_WORD[condition]}. High ${high}, low ${low}.`\n  return (\n    <Widget\n      size={size}\n      label={`Weather in ${city}`}\n      surfaceClassName={cn(\n        \"text-white [text-shadow:0_1px_2px_rgb(0_0_0/0.18)]\",\n        SKY[condition]\n      )}\n      className={className}\n      {...props}\n    >\n      <p className=\"sr-only\">{summary}</p>\n      <div\n        aria-hidden=\"true\"\n        className=\"flex size-full flex-col\"\n        style={{\n          padding: sp(size === \"small\" ? 11 : size === \"medium\" ? 9 : 13),\n        }}\n      >\n        <div className=\"flex items-start justify-between\">\n          <div className=\"min-w-0\">\n            <p\n              className=\"flex items-center gap-[calc(var(--u)*3)] font-semibold\"\n              style={fs(size === \"small\" ? 9.5 : 10)}\n            >\n              <span className=\"truncate\">{city}</span>\n              <Navigation\n                className=\"shrink-0 fill-current\"\n                style={{ width: sp(8), height: sp(8) }}\n              />\n            </p>\n            <p\n              className=\"font-light tracking-tight tabular-nums\"\n              style={{\n                ...fs(size === \"small\" ? 31 : size === \"medium\" ? 27 : 38),\n                lineHeight: 1.05,\n                marginInlineStart: sp(-1),\n              }}\n            >\n              {temperature}°\n            </p>\n          </div>\n          {size !== \"small\" && (\n            <div\n              className=\"flex flex-col items-end text-end\"\n              style={{ gap: sp(1) }}\n            >\n              <SkyIcon\n                condition={condition}\n                style={{\n                  width: sp(size === \"medium\" ? 14 : 20),\n                  height: sp(size === \"medium\" ? 14 : 20),\n                }}\n              />\n              <p className=\"font-semibold\" style={fs(9.5)}>\n                {SKY_WORD[condition]}\n              </p>\n              <p className=\"tabular-nums\" style={fs(9.5)}>\n                H:{high}° L:{low}°\n              </p>\n            </div>\n          )}\n        </div>\n        {size === \"small\" && (\n          <div className=\"mt-auto\">\n            <p\n              className=\"flex items-center font-semibold\"\n              style={{ gap: sp(4), ...fs(9.5) }}\n            >\n              <SkyIcon\n                condition={condition}\n                style={{ width: sp(12), height: sp(12) }}\n              />\n              <span className=\"truncate\">{SKY_WORD[condition]}</span>\n            </p>\n            <p className=\"tabular-nums\" style={fs(9.5)}>\n              H:{high}° L:{low}°\n            </p>\n          </div>\n        )}\n        {size !== \"small\" && hourly.length > 0 && (\n          <div\n            className={cn(\n              \"flex justify-between border-t border-white/25\",\n              size === \"medium\" ? \"mt-auto\" : \"mt-[calc(var(--u)*10)]\"\n            )}\n            style={{ paddingTop: sp(size === \"medium\" ? 5 : 8) }}\n          >\n            {hourly.slice(0, 6).map((h) => (\n              <div\n                key={h.label}\n                className=\"flex flex-col items-center\"\n                style={{\n                  gap: sp(size === \"medium\" ? 2.5 : 4),\n                  ...fs(size === \"medium\" ? 8.5 : 9),\n                }}\n              >\n                <span className=\"font-medium\">{h.label}</span>\n                <SkyIcon\n                  condition={h.condition}\n                  style={{\n                    width: sp(size === \"medium\" ? 11 : 13),\n                    height: sp(size === \"medium\" ? 11 : 13),\n                  }}\n                />\n                <span\n                  className=\"font-semibold tabular-nums\"\n                  style={fs(size === \"medium\" ? 9.5 : 10)}\n                >\n                  {h.temp}°\n                </span>\n              </div>\n            ))}\n          </div>\n        )}\n        {size === \"large\" && daily.length > 0 && (\n          <ul\n            className=\"mt-[calc(var(--u)*8)] flex flex-1 flex-col justify-between border-t border-white/25\"\n            style={{ paddingTop: sp(4) }}\n          >\n            {daily.slice(0, 5).map((d) => (\n              <li\n                key={d.day}\n                className=\"grid grid-cols-[18%_12%_1fr] items-center\"\n                style={{ gap: sp(6), ...fs(10) }}\n              >\n                <span className=\"font-semibold\">{d.day}</span>\n                <SkyIcon\n                  condition={d.condition}\n                  style={{ width: sp(12), height: sp(12) }}\n                />\n                <span className=\"flex items-center\" style={{ gap: sp(6) }}>\n                  <span className=\"w-[14%] text-end tabular-nums opacity-85\">\n                    {d.low}°\n                  </span>\n                  <span className=\"relative h-[calc(var(--u)*3.5)] flex-1 rounded-full bg-white/25\">\n                    <span\n                      className=\"absolute inset-y-0 rounded-full bg-gradient-to-r from-[oklch(0.85_0.13_200)] to-[oklch(0.85_0.15_80)]\"\n                      style={{\n                        left: `${((d.low - min) / (max - min || 1)) * 100}%`,\n                        right: `${100 - ((d.high - min) / (max - min || 1)) * 100}%`,\n                      }}\n                    />\n                  </span>\n                  <span className=\"w-[14%] tabular-nums\">{d.high}°</span>\n                </span>\n              </li>\n            ))}\n          </ul>\n        )}\n      </div>\n    </Widget>\n  )\n}\n\n// Calendar\n\nconst WEEKDAYS = [\n  \"Sunday\",\n  \"Monday\",\n  \"Tuesday\",\n  \"Wednesday\",\n  \"Thursday\",\n  \"Friday\",\n  \"Saturday\",\n]\nconst MONTHS = [\n  \"January\",\n  \"February\",\n  \"March\",\n  \"April\",\n  \"May\",\n  \"June\",\n  \"July\",\n  \"August\",\n  \"September\",\n  \"October\",\n  \"November\",\n  \"December\",\n]\n\ntype CalendarEvent = {\n  id: string\n  title: string\n  time: string\n  tone?: \"red\" | \"blue\" | \"green\" | \"orange\" | \"purple\"\n}\n\nconst EVENT_TONE: Record<NonNullable<CalendarEvent[\"tone\"]>, string> = {\n  red: \"bg-destructive\",\n  blue: \"bg-chart-1\",\n  green: \"bg-chart-3\",\n  orange: \"bg-chart-5\",\n  purple: \"bg-chart-2\",\n}\n\ntype CalendarWidgetProps = Omit<\n  WidgetProps,\n  \"label\" | \"children\" | \"surfaceClassName\"\n> & {\n  /** The day to show, as an ISO date such as \"2026-10-01\". Read as UTC, so every viewer sees the same day. */\n  date: string\n  /** Events on that day, in order. */\n  events?: CalendarEvent[]\n}\n\nfunction monthGrid(year: number, month: number) {\n  const first = new Date(Date.UTC(year, month, 1)).getUTCDay()\n  const days = new Date(Date.UTC(year, month + 1, 0)).getUTCDate()\n  return { lead: first, days }\n}\n\nfunction Events({ events, limit }: { events: CalendarEvent[]; limit: number }) {\n  if (events.length === 0)\n    return (\n      <p className=\"text-muted-foreground\" style={fs(9.5)}>\n        {\"No events today\"}\n      </p>\n    )\n  return (\n    <ul className=\"flex flex-col\" style={{ gap: sp(5) }}>\n      {events.slice(0, limit).map((e) => (\n        <li key={e.id} className=\"flex items-stretch\" style={{ gap: sp(5) }}>\n          <span\n            aria-hidden=\"true\"\n            className={cn(\n              \"w-[calc(var(--u)*2.4)] shrink-0 rounded-full\",\n              EVENT_TONE[e.tone ?? \"blue\"]\n            )}\n          />\n          <span className=\"min-w-0 leading-tight\" style={fs(9.5)}>\n            <span className=\"block truncate font-semibold\">{e.title}</span>\n            <span className=\"text-muted-foreground block truncate tabular-nums\">\n              {e.time}\n            </span>\n          </span>\n        </li>\n      ))}\n    </ul>\n  )\n}\n\n/** A calendar widget: the day in big type, a month grid with today circled, and the day's events. */\nfunction CalendarWidget({\n  date,\n  events = [],\n  size = \"small\",\n  className,\n  ...props\n}: CalendarWidgetProps) {\n  const d = new Date(`${date}T00:00:00Z`)\n  const year = d.getUTCFullYear()\n  const month = d.getUTCMonth()\n  const day = d.getUTCDate()\n  const weekday = WEEKDAYS[d.getUTCDay()]!\n  const { lead, days } = monthGrid(year, month)\n  const label = `${weekday}, ${MONTHS[month]} ${day}, ${year}`\n  const compact = size === \"medium\"\n  const cell = compact ? 8.5 : 12\n  const grid = (\n    <div\n      aria-hidden=\"true\"\n      className=\"flex flex-col\"\n      style={{ gap: sp(compact ? 1 : 2) }}\n    >\n      <p className=\"text-destructive font-semibold\" style={fs(9.5)}>\n        {MONTHS[month]!.toUpperCase()}\n      </p>\n      <div\n        className=\"grid grid-cols-7 text-center tabular-nums\"\n        style={{ ...fs(compact ? 7.5 : 8.5), rowGap: sp(compact ? 1 : 2) }}\n      >\n        {[\"S\", \"M\", \"T\", \"W\", \"T\", \"F\", \"S\"].map((w, i) => (\n          <span key={i} className=\"text-muted-foreground\">\n            {w}\n          </span>\n        ))}\n        {Array.from({ length: lead }, (_, i) => (\n          <span key={`lead-${i}`} />\n        ))}\n        {Array.from({ length: days }, (_, i) => (\n          <span\n            key={i}\n            className={cn(\n              \"mx-auto flex items-center justify-center rounded-full font-medium\",\n              i + 1 === day &&\n                \"bg-destructive dark:text-background font-bold text-white\"\n            )}\n            style={{ width: sp(cell), height: sp(cell) }}\n          >\n            {i + 1}\n          </span>\n        ))}\n      </div>\n    </div>\n  )\n  return (\n    <Widget\n      size={size}\n      label={`Calendar, ${label}`}\n      surfaceClassName=\"bg-background text-foreground\"\n      className={className}\n      {...props}\n    >\n      <div\n        className={cn(\n          \"size-full\",\n          size === \"large\"\n            ? \"flex flex-col\"\n            : size === \"medium\"\n              ? \"grid grid-cols-2\"\n              : \"flex flex-col\"\n        )}\n        style={{\n          padding: sp(size === \"small\" ? 11 : size === \"medium\" ? 9 : 13),\n          gap: sp(10),\n        }}\n      >\n        {size === \"large\" ? (\n          <>\n            {grid}\n            <div className=\"border-t\" style={{ paddingTop: sp(8) }}>\n              <p className=\"sr-only\">{label}</p>\n              <Events events={events} limit={3} />\n            </div>\n          </>\n        ) : (\n          <>\n            <div className=\"flex min-w-0 flex-col\" style={{ gap: sp(4) }}>\n              <p className=\"sr-only\">{label}</p>\n              <p\n                aria-hidden=\"true\"\n                className=\"text-destructive font-semibold tracking-wide uppercase\"\n                style={fs(9.5)}\n              >\n                {weekday}\n              </p>\n              <p\n                aria-hidden=\"true\"\n                className=\"font-light tracking-tight tabular-nums\"\n                style={{ ...fs(size === \"small\" ? 28 : 34), lineHeight: 1 }}\n              >\n                {day}\n              </p>\n              <div className=\"mt-auto\">\n                <Events events={events} limit={1} />\n              </div>\n            </div>\n            {size === \"medium\" && grid}\n          </>\n        )}\n      </div>\n    </Widget>\n  )\n}\n\n// Battery\n\ntype BatteryDevice = {\n  id: string\n  /** Device name. */\n  name: string\n  /** Charge from 0 to 100. */\n  level: number\n  /** Whether it is charging. */\n  charging?: boolean\n  /** Icon for the device. */\n  icon: React.ReactNode\n}\n\ntype BatteryWidgetProps = Omit<\n  WidgetProps,\n  \"label\" | \"children\" | \"surfaceClassName\"\n> & {\n  /** Devices and their charge. Small shows four, medium four, large lists them all. */\n  devices: BatteryDevice[]\n}\n\nfunction Ring({ device, big }: { device: BatteryDevice; big?: boolean }) {\n  const r = 20\n  const c = 2 * Math.PI * r\n  const level = Math.max(0, Math.min(100, device.level))\n  const low = level <= 20 && !device.charging\n  return (\n    <div\n      aria-hidden=\"true\"\n      className=\"relative flex aspect-square w-full items-center justify-center\"\n    >\n      <svg\n        viewBox=\"0 0 48 48\"\n        className=\"absolute inset-0 size-full -rotate-90\"\n      >\n        <circle\n          cx=\"24\"\n          cy=\"24\"\n          r={r}\n          fill=\"none\"\n          strokeWidth=\"4.5\"\n          className=\"stroke-foreground/15\"\n        />\n        <circle\n          cx=\"24\"\n          cy=\"24\"\n          r={r}\n          fill=\"none\"\n          strokeWidth=\"4.5\"\n          strokeLinecap=\"round\"\n          strokeDasharray={`${(level / 100) * c} ${c}`}\n          className={cn(low ? \"stroke-destructive\" : \"stroke-chart-3\")}\n        />\n      </svg>\n      <span\n        className=\"text-foreground flex items-center justify-center [&_svg]:size-full\"\n        style={{ width: big ? \"34%\" : \"38%\", height: big ? \"34%\" : \"38%\" }}\n      >\n        {device.icon}\n      </span>\n      {device.charging && (\n        <span className=\"bg-background text-chart-3 absolute -end-[4%] -bottom-[4%] flex size-[34%] items-center justify-center rounded-full shadow-sm\">\n          <Zap className=\"size-[70%] fill-current\" />\n        </span>\n      )}\n    </div>\n  )\n}\n\n/** A battery widget: one ring per device, red when low, with a bolt while charging. The large size lists devices with bars. */\nfunction BatteryWidget({\n  devices,\n  size = \"small\",\n  className,\n  ...props\n}: BatteryWidgetProps) {\n  const spoken = (d: BatteryDevice) =>\n    `${d.name}, ${Math.round(d.level)} percent${d.charging ? \", charging\" : \"\"}`\n  return (\n    <Widget\n      size={size}\n      label=\"Batteries\"\n      surfaceClassName=\"bg-background text-foreground\"\n      className={className}\n      {...props}\n    >\n      <ul\n        className={cn(\n          \"size-full\",\n          size === \"large\"\n            ? \"flex flex-col justify-around\"\n            : size === \"medium\"\n              ? \"grid grid-cols-4 items-center\"\n              : \"grid grid-cols-2 grid-rows-2 items-center\"\n        )}\n        style={{\n          padding: sp(size === \"large\" ? 14 : size === \"small\" ? 9 : 13),\n          gap: sp(size === \"large\" ? 4 : size === \"small\" ? 5 : 10),\n        }}\n      >\n        {devices.slice(0, size === \"large\" ? 6 : 4).map((d) =>\n          size === \"large\" ? (\n            <li\n              key={d.id}\n              className=\"flex items-center\"\n              style={{ gap: sp(10) }}\n            >\n              <span\n                aria-hidden=\"true\"\n                className=\"shrink-0\"\n                style={{ width: sp(30) }}\n              >\n                <Ring device={d} big />\n              </span>\n              <span className=\"min-w-0 flex-1\">\n                <span className=\"sr-only\">{spoken(d)}</span>\n                <span\n                  aria-hidden=\"true\"\n                  className=\"block truncate font-semibold\"\n                  style={fs(11)}\n                >\n                  {d.name}\n                </span>\n                <span\n                  aria-hidden=\"true\"\n                  className=\"bg-foreground/15 mt-[calc(var(--u)*3)] block h-[calc(var(--u)*4)] overflow-hidden rounded-full\"\n                >\n                  <span\n                    className={cn(\n                      \"block h-full rounded-full\",\n                      d.level <= 20 && !d.charging\n                        ? \"bg-destructive\"\n                        : \"bg-chart-3\"\n                    )}\n                    style={{ width: `${d.level}%` }}\n                  />\n                </span>\n              </span>\n              <span\n                aria-hidden=\"true\"\n                className=\"font-semibold tabular-nums\"\n                style={fs(11)}\n              >\n                {Math.round(d.level)}%\n              </span>\n            </li>\n          ) : (\n            <li\n              key={d.id}\n              className=\"flex flex-col items-center\"\n              style={{ gap: sp(3) }}\n            >\n              <span className=\"sr-only\">{spoken(d)}</span>\n              <span\n                className=\"w-full\"\n                style={{ maxWidth: sp(size === \"small\" ? 24 : 40) }}\n              >\n                <Ring device={d} />\n              </span>\n              <span\n                aria-hidden=\"true\"\n                className=\"font-semibold tabular-nums\"\n                style={fs(size === \"small\" ? 9.5 : 10)}\n              >\n                {Math.round(d.level)}%\n              </span>\n            </li>\n          )\n        )}\n      </ul>\n    </Widget>\n  )\n}\n\nexport {\n  Widget,\n  WeatherWidget,\n  CalendarWidget,\n  BatteryWidget,\n  type WidgetProps,\n  type WidgetSize,\n  type WeatherWidgetProps,\n  type WeatherCondition,\n  type HourlyForecast,\n  type DailyForecast,\n  type CalendarWidgetProps,\n  type CalendarEvent,\n  type BatteryWidgetProps,\n  type BatteryDevice,\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}