{
  "!meta": {
    "_comment": "Tables the documentation renderers used to hardcode, one copy each, in two languages. They live here so there is one place to change them and no way for the app and the website to disagree.",
    "categories": [
      {
        "id": "primitives",
        "label": "Primitives"
      },
      {
        "id": "systemAPIs",
        "label": "System APIs"
      },
      {
        "id": "uiComponents",
        "label": "UI Components"
      },
      {
        "id": "enums",
        "label": "Enums"
      },
      {
        "id": "reference",
        "label": "Reference"
      }
    ],
    "typeNames": {
      "+RootlessDateTime": "DateTime",
      "+RootlessDuration": "Duration",
      "+RootlessColor": "Color",
      "+RootlessSize": "Size",
      "+EdgeInsetsObject": "EdgeInsets",
      "+AlignmentObject": "Alignment",
      "+HttpResponse": "HttpResponse",
      "+Coordinate": "Coordinate",
      "+CalendarObject": "Calendar",
      "+EventObject": "Event",
      "+DirectoryObject": "Directory",
      "+FileObject": "File",
      "+WidgetNode": "WidgetNode",
      "+ChildrenProxy": "ChildrenProxy",
      "bool": "Boolean",
      "number": "Number",
      "string": "String",
      "?": "Any",
      "+RootlessAnimation": "Animation",
      "+RootlessCanvas": "Canvas"
    },
    "apiPrototypes": {
      "DateTime": "RootlessDateTime.prototype",
      "Duration": "RootlessDuration.prototype",
      "Color": "RootlessColor.prototype",
      "Size": "RootlessSize.prototype",
      "EdgeInsets": "EdgeInsetsObject.prototype",
      "Alignment": "AlignmentObject.prototype",
      "Widget": "WidgetNode.prototype",
      "Calendar": "CalendarObject.prototype",
      "Event": "EventObject.prototype",
      "Location": "Coordinate.prototype",
      "WebView": "RootlessWebView.prototype",
      "Reminder": "RootlessReminder.prototype",
      "Distance": "RootlessDistance.prototype",
      "Font": "RootlessFont.prototype",
      "Animation": "RootlessAnimation.prototype",
      "Canvas": "RootlessCanvas.prototype"
    },
    "prototypeNames": {
      "HttpResponse.prototype": "HttpResponse",
      "Coordinate.prototype": "Coordinate",
      "CallbackResponse.prototype": "CallbackResponse",
      "CalendarObject.prototype": "CalendarObject",
      "EventObject.prototype": "EventObject",
      "DirectoryObject.prototype": "Directory",
      "FileObject.prototype": "File",
      "ChildrenProxy.prototype": "ChildrenProxy",
      "GeoResult": "GeoResult",
      "EventQuery": "EventQuery",
      "Attendee": "Attendee",
      "RecurrenceRule": "RecurrenceRule",
      "DeviceInfo": "DeviceInfo",
      "BatteryInfo": "BatteryInfo",
      "ScreenInfo": "ScreenInfo",
      "VolumeInfo": "VolumeInfo"
    }
  },
  "!name": "rootless",
  "!define": {
    "HttpResponse.prototype": {
      "!category": "reference",
      "ok": {
        "!type": "bool",
        "!doc": "True if status 200-299"
      },
      "status": {
        "!type": "number",
        "!doc": "HTTP status code"
      },
      "text": {
        "!type": "string",
        "!doc": "Body as string"
      },
      "json": {
        "!type": "?",
        "!doc": "Parsed JSON or null"
      },
      "headers": {
        "!type": "?",
        "!doc": "Response headers (lowercase keys)"
      },
      "url": {
        "!type": "string",
        "!doc": "Final URL (after redirects)"
      },
      "stale": {
        "!type": "bool",
        "!doc": "True if from stale cache"
      },
      "mimeType": {
        "!type": "string",
        "!doc": "Response MIME type"
      },
      "error": {
        "!type": "string",
        "!doc": "Why the request failed, or absent when it did not: \"timeout\", \"network_error\" or \"server_error\". Http never throws for a transport failure. Check ok and branch on this."
      },
      "!subtitle": "What a request returns",
      "!description": "Returned by every Http call. text and json read the body; ok is true for a 2xx status."
    },
    "Coordinate.prototype": {
      "latitude": {
        "!type": "number",
        "!doc": "Latitude"
      },
      "longitude": {
        "!type": "number",
        "!doc": "Longitude"
      },
      "altitude": {
        "!type": "number",
        "!doc": "Altitude"
      },
      "accuracy": {
        "horizontal": "number",
        "vertical": "number"
      },
      "reverseGeocode": {
        "!type": "fn(locale?: string) -> [GeoResult]",
        "!doc": "Reverse geocode this coordinate to get address information."
      },
      "formattedAddress": {
        "!type": "fn(locale?: string) -> string",
        "!doc": "Get a formatted address string for this coordinate."
      },
      "distanceTo": {
        "!type": "fn(other: +Coordinate) -> number",
        "!doc": "Distance to another coordinate in kilometers."
      },
      "isWithin": {
        "!type": "fn(radiusKm: number, center: +Coordinate) -> bool",
        "!doc": "Check if this coordinate is within a radius of another."
      },
      "bearingTo": {
        "!type": "fn(other: +Coordinate) -> number",
        "!doc": "Bearing in degrees (0-360) to another coordinate."
      },
      "openInMaps": {
        "!type": "fn(label?: string)",
        "!doc": "Open this coordinate in Apple Maps."
      }
    },
    "GeoResult": {
      "!category": "reference",
      "name": "string",
      "thoroughfare": "string",
      "subThoroughfare": "string",
      "locality": "string",
      "subLocality": "string",
      "administrativeArea": "string",
      "subAdministrativeArea": "string",
      "postalCode": "string",
      "country": "string",
      "isoCountryCode": "string",
      "timeZone": "string",
      "!subtitle": "A reverse-geocoded address",
      "!description": "Returned by Location.reverse. Every field is optional: what a place has depends on where it is."
    },
    "CallbackResponse.prototype": {
      "!category": "reference",
      "ok": {
        "!type": "bool",
        "!doc": "True if the target app responded with x-success"
      },
      "params": {
        "!type": "?",
        "!doc": "Response parameters from the target app (success only)"
      },
      "error": {
        "!type": "string",
        "!doc": "Error message: \"cancelled\", \"timeout\", \"invalid_url\", or error from target app"
      },
      "!subtitle": "What an x-callback-url call returns",
      "!description": "ok is false when the other app reported an error or the user cancelled. params carries whatever it sent back."
    },
    "CalendarObject.prototype": {
      "identifier": {
        "!type": "string",
        "!doc": "Calendar identifier"
      },
      "title": {
        "!type": "string",
        "!doc": "Calendar title"
      },
      "isSubscribed": {
        "!type": "bool",
        "!doc": "Whether this is a subscribed calendar"
      },
      "allowsContentModifications": {
        "!type": "bool",
        "!doc": "Whether this calendar allows modifications"
      },
      "color": {
        "!type": "+RootlessColor",
        "!doc": "Calendar color"
      },
      "getEvents": {
        "!type": "fn(query?: EventQuery) -> [+EventObject]",
        "!doc": "Fetch events from this calendar."
      },
      "supportsAvailability": {
        "!type": "fn(availability: string) -> bool",
        "!doc": "Check if this calendar supports a given availability type."
      },
      "save": {
        "!type": "fn()",
        "!doc": "Save changes to this calendar."
      },
      "remove": {
        "!type": "fn()",
        "!doc": "Delete this calendar."
      }
    },
    "EventQuery": {
      "!category": "reference",
      "timeframe": "string",
      "from": "+RootlessDateTime",
      "to": "+RootlessDateTime",
      "limit": "number",
      "!subtitle": "A query for events",
      "!description": "Passed to Event.fetch. timeframe is a shorthand that fills from and to."
    },
    "EventObject.prototype": {
      "identifier": {
        "!type": "string",
        "!doc": "Event identifier"
      },
      "title": {
        "!type": "string",
        "!doc": "Event title"
      },
      "location": {
        "!type": "string",
        "!doc": "Event location"
      },
      "notes": {
        "!type": "string",
        "!doc": "Event notes"
      },
      "startDate": {
        "!type": "+RootlessDateTime",
        "!doc": "Event start date"
      },
      "endDate": {
        "!type": "+RootlessDateTime",
        "!doc": "Event end date"
      },
      "isAllDay": {
        "!type": "bool",
        "!doc": "Whether this is an all-day event"
      },
      "availability": {
        "!type": "string",
        "!doc": "Event availability"
      },
      "timeZone": {
        "!type": "string",
        "!doc": "Event time zone"
      },
      "calendar": {
        "!type": "+CalendarObject",
        "!doc": "Calendar this event belongs to"
      },
      "attendees": {
        "!type": "[Attendee]",
        "!doc": "Event attendees"
      },
      "save": {
        "!type": "fn() -> +EventObject",
        "!doc": "Save this event."
      },
      "remove": {
        "!type": "fn()",
        "!doc": "Delete this event."
      },
      "setRecurrence": {
        "!type": "fn(rule: RecurrenceRule)",
        "!doc": "Set a recurrence rule on this event."
      },
      "clearRecurrences": {
        "!type": "fn()",
        "!doc": "Remove all recurrence rules from this event."
      },
      "presentEdit": {
        "!type": "fn() -> +EventObject",
        "!doc": "Present the system event editor."
      }
    },
    "Attendee": {
      "!category": "reference",
      "isCurrentUser": "bool",
      "name": "string",
      "role": "string",
      "type": "string",
      "status": "string",
      "!subtitle": "Someone invited to an event",
      "!description": "Read from an event's attendees. Read-only: EventKit does not allow changing them."
    },
    "RecurrenceRule": {
      "!category": "reference",
      "frequency": "string",
      "interval": "number",
      "end": "?",
      "!subtitle": "How an event repeats",
      "!description": "Built by Event.recurrence and read back from a repeating event."
    },
    "DirectoryObject.prototype": {
      "!category": "reference",
      "path": {
        "!type": "string",
        "!doc": "Directory path"
      },
      "name": {
        "!type": "string",
        "!doc": "Directory name"
      },
      "exists": {
        "!type": "bool",
        "!doc": "Whether this directory exists"
      },
      "documents": {
        "!type": "+DirectoryObject",
        "!doc": "Documents subdirectory"
      },
      "cache": {
        "!type": "+DirectoryObject",
        "!doc": "Cache subdirectory"
      },
      "temp": {
        "!type": "+DirectoryObject",
        "!doc": "Temp subdirectory"
      },
      "library": {
        "!type": "+DirectoryObject",
        "!doc": "Library subdirectory"
      },
      "dir": {
        "!type": "fn(name: string) -> +DirectoryObject",
        "!doc": "Get a subdirectory by name."
      },
      "file": {
        "!type": "fn(name: string) -> +FileObject",
        "!doc": "Get a file by name."
      },
      "list": {
        "!type": "fn() -> [?]",
        "!doc": "List all files and directories."
      },
      "create": {
        "!type": "fn(recursive?: bool)",
        "!doc": "Create this directory."
      },
      "delete": {
        "!type": "fn()",
        "!doc": "Delete this directory."
      },
      "!subtitle": "A folder",
      "!description": "Returned by FileSystem. dir() and file() reach inside it; list() enumerates it."
    },
    "FileObject.prototype": {
      "!category": "reference",
      "path": {
        "!type": "string",
        "!doc": "File path"
      },
      "name": {
        "!type": "string",
        "!doc": "File name"
      },
      "extension": {
        "!type": "string",
        "!doc": "File extension"
      },
      "exists": {
        "!type": "bool",
        "!doc": "Whether this file exists"
      },
      "size": {
        "!type": "number",
        "!doc": "File size in bytes"
      },
      "uti": {
        "!type": "string",
        "!doc": "Uniform type identifier"
      },
      "createdAt": {
        "!type": "+RootlessDateTime",
        "!doc": "File creation date"
      },
      "modifiedAt": {
        "!type": "+RootlessDateTime",
        "!doc": "File modification date"
      },
      "iCloud": {
        "!type": "?",
        "!doc": "iCloud metadata"
      },
      "tags": {
        "!type": "[string]",
        "!doc": "File tags"
      },
      "attributes": {
        "!type": "?",
        "!doc": "File attributes"
      },
      "read": {
        "!type": "fn(as?: string) -> ?",
        "!doc": "Read file contents. Modes: 'string', 'json', 'image', 'data'."
      },
      "write": {
        "!type": "fn(content: ?)",
        "!doc": "Write content to this file."
      },
      "append": {
        "!type": "fn(content: string)",
        "!doc": "Append text to this file."
      },
      "copyTo": {
        "!type": "fn(destination: ?)",
        "!doc": "Copy this file to a destination."
      },
      "moveTo": {
        "!type": "fn(destination: ?)",
        "!doc": "Move this file to a destination."
      },
      "delete": {
        "!type": "fn()",
        "!doc": "Delete this file."
      },
      "download": {
        "!type": "fn() -> bool",
        "!doc": "Trigger iCloud download for this file."
      },
      "!subtitle": "A file",
      "!description": "Returned by a directory's file(). Reads and writes text, JSON, images and raw data, and reports size, dates and extended attributes."
    },
    "RootlessDateTime.prototype": {
      "year": {
        "!type": "number",
        "!doc": "Year component"
      },
      "month": {
        "!type": "number",
        "!doc": "Month component (1-12)"
      },
      "day": {
        "!type": "number",
        "!doc": "Day of month"
      },
      "hour": {
        "!type": "number",
        "!doc": "Hour component (0-23)"
      },
      "minute": {
        "!type": "number",
        "!doc": "Minute component (0-59)"
      },
      "second": {
        "!type": "number",
        "!doc": "Second component (0-59)"
      },
      "weekday": {
        "!type": "string",
        "!doc": "Day name (\"sunday\" to \"saturday\")"
      },
      "format": {
        "!type": "fn(pattern: string) -> string",
        "!doc": "Format this date using a pattern (e.g. 'yyyy-MM-dd HH:mm')."
      },
      "relative": {
        "!type": "fn() -> string",
        "!doc": "Get a relative time string (e.g. '2 hours ago')."
      },
      "isBefore": {
        "!type": "fn(other: +RootlessDateTime) -> bool",
        "!doc": "Check if this date is before another."
      },
      "isAfter": {
        "!type": "fn(other: +RootlessDateTime) -> bool",
        "!doc": "Check if this date is after another."
      },
      "isSameDay": {
        "!type": "fn(other: +RootlessDateTime) -> bool",
        "!doc": "Check if this date is the same day as another."
      },
      "add": {
        "!type": "fn(duration: +RootlessDuration) -> +RootlessDateTime",
        "!doc": "Add a duration to this date."
      },
      "subtract": {
        "!type": "fn(duration: +RootlessDuration) -> +RootlessDateTime",
        "!doc": "Subtract a duration from this date."
      },
      "difference": {
        "!type": "fn(other: +RootlessDateTime) -> +RootlessDuration",
        "!doc": "Get the duration between this date and another."
      }
    },
    "RootlessDuration.prototype": {
      "inSeconds": {
        "!type": "number",
        "!doc": "Duration in seconds"
      },
      "inMinutes": {
        "!type": "number",
        "!doc": "Duration in minutes"
      },
      "inHours": {
        "!type": "number",
        "!doc": "Duration in hours"
      },
      "add": {
        "!type": "fn(other: +RootlessDuration) -> +RootlessDuration",
        "!doc": "Add another duration to this one."
      }
    },
    "RootlessColor.prototype": {
      "r": {
        "!type": "number",
        "!doc": "Red component (0-1)"
      },
      "g": {
        "!type": "number",
        "!doc": "Green component (0-1)"
      },
      "b": {
        "!type": "number",
        "!doc": "Blue component (0-1)"
      },
      "a": {
        "!type": "number",
        "!doc": "Alpha component (0-1)"
      },
      "opacity": {
        "!type": "fn(value: number) -> +RootlessColor",
        "!doc": "Return a copy with the given opacity (0-1)."
      },
      "lighter": {
        "!type": "fn(amount: number) -> +RootlessColor",
        "!doc": "Return a lighter version of this color."
      },
      "darker": {
        "!type": "fn(amount: number) -> +RootlessColor",
        "!doc": "Return a darker version of this color."
      }
    },
    "RootlessSize.prototype": {
      "value": {
        "!type": "number",
        "!doc": "The numeric size value"
      },
      "unit": {
        "!type": "string",
        "!doc": "\"fixed\", \"infinity\", or \"fraction\""
      }
    },
    "EdgeInsetsObject.prototype": {
      "top": {
        "!type": "number",
        "!doc": "Top inset"
      },
      "leading": {
        "!type": "number",
        "!doc": "Leading (left) inset"
      },
      "bottom": {
        "!type": "number",
        "!doc": "Bottom inset"
      },
      "trailing": {
        "!type": "number",
        "!doc": "Trailing (right) inset"
      }
    },
    "WidgetNode.prototype": {},
    "DeviceInfo": {
      "!category": "reference",
      "name": {
        "!type": "string",
        "!doc": "Device name (editable in system settings)"
      },
      "systemName": {
        "!type": "string",
        "!doc": "Operating system name (e.g. \"iOS\")"
      },
      "systemVersion": {
        "!type": "string",
        "!doc": "Operating system version"
      },
      "model": {
        "!type": "string",
        "!doc": "Device model (e.g. \"iPhone\", \"iPad\")"
      },
      "isPhone": {
        "!type": "bool",
        "!doc": "Whether the device is an iPhone"
      },
      "isPad": {
        "!type": "bool",
        "!doc": "Whether the device is an iPad"
      },
      "preferredLanguages": {
        "!type": "[string]",
        "!doc": "Ordered list of preferred languages from system settings"
      },
      "locale": {
        "!type": "string",
        "!doc": "Device locale identifier (e.g. \"en_US\")"
      },
      "language": {
        "!type": "string",
        "!doc": "Primary preferred language identifier"
      },
      "!subtitle": "The device",
      "!description": "Read from Device.info."
    },
    "BatteryInfo": {
      "!category": "reference",
      "level": {
        "!type": "number",
        "!doc": "Battery level as a percentage (0 to 1)"
      },
      "state": {
        "!type": "string",
        "!doc": "Battery state: \"charging\", \"full\", \"unplugged\", or \"unknown\""
      },
      "isLowPower": {
        "!type": "bool",
        "!doc": "Whether Low Power Mode is enabled"
      },
      "!subtitle": "Battery state",
      "!description": "Read from Device.battery. level is 0 to 1."
    },
    "ScreenInfo": {
      "!category": "reference",
      "brightness": {
        "!type": "number",
        "!doc": "Screen brightness (0 to 1)"
      },
      "width": {
        "!type": "number",
        "!doc": "Screen width in points (accounts for rotation)"
      },
      "height": {
        "!type": "number",
        "!doc": "Screen height in points (accounts for rotation)"
      },
      "nativeWidth": {
        "!type": "number",
        "!doc": "Screen width in pixels (does not account for rotation)"
      },
      "nativeHeight": {
        "!type": "number",
        "!doc": "Screen height in pixels (does not account for rotation)"
      },
      "scale": {
        "!type": "number",
        "!doc": "Screen scale factor (e.g. 2.0 for Retina, 3.0 for Super Retina)"
      },
      "appearance": {
        "!type": "string",
        "!doc": "Current appearance: \"dark\" or \"light\""
      },
      "orientation": {
        "!type": "string",
        "!doc": "Device orientation: \"portrait\", \"portraitUpsideDown\", \"landscapeLeft\", \"landscapeRight\", \"faceUp\", \"faceDown\", or \"unknown\""
      },
      "setBrightness": {
        "!type": "fn(percentage: number)",
        "!doc": "Set screen brightness (0-1)."
      },
      "!subtitle": "The screen",
      "!description": "Read from Device.screen. Points, not pixels, except nativeWidth and nativeHeight."
    },
    "VolumeInfo": {
      "!category": "reference",
      "level": {
        "!type": "number",
        "!doc": "Device volume (0 to 1)"
      },
      "set": {
        "!type": "fn(percentage: number)",
        "!doc": "Set system volume (0-1)."
      },
      "!subtitle": "System volume",
      "!description": "Read from Device.volume. level is 0 to 1."
    },
    "RootlessDistance.prototype": {
      "inMeters": {
        "!type": "number",
        "!doc": "The distance in metres."
      },
      "inKilometers": {
        "!type": "number",
        "!doc": "The distance in kilometres."
      },
      "inMiles": {
        "!type": "number",
        "!doc": "The distance in miles."
      },
      "inFeet": {
        "!type": "number",
        "!doc": "The distance in feet."
      },
      "formatted": {
        "!type": "fn(options?: ?) -> string",
        "!doc": "Localised text for this distance.",
        "!params": [
          {
            "name": "options",
            "type": "?",
            "doc": "",
            "fields": [
              {
                "name": "to",
                "type": "string",
                "doc": "Convert to this unit. Left out, the region decides."
              },
              {
                "name": "style",
                "type": "string",
                "doc": "short, medium or long."
              },
              {
                "name": "digits",
                "type": "number",
                "doc": "Maximum decimal places."
              }
            ]
          }
        ]
      }
    },
    "RootlessFont.prototype": {
      "withWeight": {
        "!type": "fn(weight: string) -> +RootlessFont",
        "!doc": "A copy at a different weight, e.g. Font.CAPTION.withWeight(FontWeight.bold)."
      },
      "withFamily": {
        "!type": "fn(name: string) -> +RootlessFont",
        "!doc": "A copy in a different family."
      },
      "withDesign": {
        "!type": "fn(design: string) -> +RootlessFont",
        "!doc": "A copy in a system-font design - rounded, serif or monospaced."
      },
      "rounded": {
        "!type": "fn() -> +RootlessFont",
        "!doc": "Shorthand for withDesign(FontDesign.rounded)."
      },
      "serif": {
        "!type": "fn() -> +RootlessFont",
        "!doc": "Shorthand for withDesign(FontDesign.serif)."
      },
      "monospaced": {
        "!type": "fn() -> +RootlessFont",
        "!doc": "Shorthand for withDesign(FontDesign.monospaced). Keeps digits from shifting width as a number changes."
      }
    },
    "RootlessReminder.prototype": {
      "identifier": {
        "!type": "string",
        "!doc": "Stable id. Empty until the reminder has been saved."
      },
      "title": {
        "!type": "string",
        "!doc": "What it says."
      },
      "notes": {
        "!type": "string",
        "!doc": "Longer text, or null."
      },
      "isCompleted": {
        "!type": "bool",
        "!doc": "Whether it is done."
      },
      "completionDate": {
        "!type": "+RootlessDateTime",
        "!doc": "When it was completed, or null."
      },
      "priority": {
        "!type": "number",
        "!doc": "0 none, 1-4 high, 5 medium, 6-9 low."
      },
      "dueDate": {
        "!type": "+RootlessDateTime",
        "!doc": "When it is due, or null."
      },
      "list": {
        "!type": "?",
        "!doc": "The list it belongs to."
      },
      "url": {
        "!type": "string",
        "!doc": "An associated URL, or null."
      },
      "save": {
        "!type": "fn() -> +RootlessReminder",
        "!doc": "Writes changes, creating the reminder if it is new. Returns itself with the stored values filled in."
      },
      "remove": {
        "!type": "fn()",
        "!doc": "Deletes it."
      },
      "complete": {
        "!type": "fn(done?: bool) -> +RootlessReminder",
        "!doc": "Marks it done, or pass false to un-complete it. Saves immediately."
      }
    },
    "RootlessWebView.prototype": {
      "loadURL": {
        "!type": "fn(url: string, options?: ?) -> +RootlessWebView",
        "!doc": "Loads a page and blocks until it has finished.",
        "!params": [
          {
            "name": "url",
            "type": "string",
            "doc": "The page to load.",
            "required": true
          },
          {
            "name": "options",
            "type": "?",
            "doc": "",
            "fields": [
              {
                "name": "timeout",
                "type": "+RootlessDuration",
                "doc": "How long to wait. Default 30 seconds."
              }
            ]
          }
        ]
      },
      "loadHTML": {
        "!type": "fn(html: string, options?: ?) -> +RootlessWebView",
        "!doc": "Loads markup instead of fetching a page.",
        "!params": [
          {
            "name": "html",
            "type": "string",
            "doc": "The markup.",
            "required": true
          },
          {
            "name": "options",
            "type": "?",
            "doc": "",
            "fields": [
              {
                "name": "baseURL",
                "type": "string",
                "doc": "Where relative links and assets resolve from."
              },
              {
                "name": "timeout",
                "type": "+RootlessDuration",
                "doc": "How long to wait for the load."
              }
            ]
          }
        ]
      },
      "evaluate": {
        "!type": "fn(script: string) -> ?",
        "!doc": "Runs JavaScript inside the loaded page and returns its result.",
        "!params": [
          {
            "name": "script",
            "type": "string",
            "doc": "The last expression is what comes back.",
            "required": true
          }
        ],
        "!returns": "Anything JSON-able. A DOM node or a function does not cross and comes back null."
      },
      "present": {
        "!type": "fn(options?: ?) -> +RootlessWebView",
        "!doc": "Shows the loaded page as a bottom sheet, and returns when it is dismissed.",
        "!params": [
          {
            "name": "options",
            "type": "?",
            "doc": "",
            "fields": [
              {
                "name": "detents",
                "type": "[?]",
                "doc": "The heights it can rest at: \"medium\", \"large\", or a fraction between 0 and 1."
              },
              {
                "name": "grabber",
                "type": "bool",
                "doc": "Show the drag handle at the top."
              },
              {
                "name": "cornerRadius",
                "type": "number",
                "doc": "Corner radius of the sheet."
              }
            ]
          }
        ]
      },
      "close": {
        "!type": "fn()",
        "!doc": "Releases the web view. Worth doing explicitly: they are not small."
      }
    },
    "RootlessAnimation.prototype": {
      "!doc": "An animation. The modifiers return a new one, so they chain.",
      "delay": {
        "!type": "fn(seconds: ?) -> +RootlessAnimation",
        "!doc": "Waits before starting. Counts against the same two-second ceiling.",
        "!params": [
          {
            "name": "seconds",
            "type": "+RootlessDuration",
            "doc": "A Duration, or a plain number of seconds.",
            "required": true
          }
        ]
      },
      "speed": {
        "!type": "fn(multiplier: number) -> +RootlessAnimation",
        "!doc": "Scales the duration. 2 runs it twice as fast.",
        "!params": [
          {
            "name": "multiplier",
            "type": "number",
            "doc": "Above 1 is faster, below 1 is slower.",
            "required": true
          }
        ]
      },
      "repeatCount": {
        "!type": "fn(count: number) -> +RootlessAnimation",
        "!doc": "Repeats. The two-second ceiling still applies to the whole thing, so a large count will be cut off.",
        "!params": [
          {
            "name": "count",
            "type": "number",
            "doc": "How many times.",
            "required": true
          }
        ]
      }
    },
    "RootlessCanvas.prototype": {
      "!doc": "A canvas. Draw into it, then render it.",
      "width": {
        "!type": "number",
        "!doc": "The width it was created with, after clamping.",
        "!readonly": true
      },
      "height": {
        "!type": "number",
        "!doc": "The height it was created with, after clamping.",
        "!readonly": true
      },
      "draw": {
        "!type": "?",
        "!doc": "The drawing operations: rect, ellipse, text and image."
      },
      "render": {
        "!type": "fn() -> +WidgetNode",
        "!doc": "Rasterises everything drawn so far and returns it as an image.",
        "!returns": "An image node, ready for Script.setWidget, an Image node, or Image.resize."
      },
      "draw.rect": {
        "!type": "fn(rect: ?, style?: ?)",
        "!doc": "A rectangle.",
        "!params": [
          {
            "name": "rect",
            "type": "?",
            "doc": "The box to draw in.",
            "required": true,
            "fields": [
              {
                "name": "x",
                "type": "number",
                "doc": "Left edge. Default 0."
              },
              {
                "name": "y",
                "type": "number",
                "doc": "Top edge. Default 0."
              },
              {
                "name": "w",
                "type": "number",
                "doc": "Width. Default 0, which draws nothing."
              },
              {
                "name": "h",
                "type": "number",
                "doc": "Height. Default 0, which draws nothing."
              }
            ]
          },
          {
            "name": "style",
            "type": "?",
            "doc": "",
            "fields": [
              {
                "name": "fill",
                "type": "+RootlessColor",
                "doc": "Fill colour. Left out, the shape is not filled."
              },
              {
                "name": "stroke",
                "type": "+RootlessColor",
                "doc": "Outline colour. Left out, there is no outline."
              },
              {
                "name": "lineWidth",
                "type": "number",
                "doc": "Outline thickness. Default 1."
              }
            ]
          }
        ]
      },
      "draw.ellipse": {
        "!type": "fn(rect: ?, style?: ?)",
        "!doc": "An ellipse filling the rectangle. A square rectangle gives a circle.",
        "!params": [
          {
            "name": "rect",
            "type": "?",
            "doc": "The box to draw in.",
            "required": true,
            "fields": [
              {
                "name": "x",
                "type": "number",
                "doc": "Left edge. Default 0."
              },
              {
                "name": "y",
                "type": "number",
                "doc": "Top edge. Default 0."
              },
              {
                "name": "w",
                "type": "number",
                "doc": "Width. Default 0, which draws nothing."
              },
              {
                "name": "h",
                "type": "number",
                "doc": "Height. Default 0, which draws nothing."
              }
            ]
          },
          {
            "name": "style",
            "type": "?",
            "doc": "",
            "fields": [
              {
                "name": "fill",
                "type": "+RootlessColor",
                "doc": "Fill colour. Left out, the shape is not filled."
              },
              {
                "name": "stroke",
                "type": "+RootlessColor",
                "doc": "Outline colour. Left out, there is no outline."
              },
              {
                "name": "lineWidth",
                "type": "number",
                "doc": "Outline thickness. Default 1."
              }
            ]
          }
        ]
      },
      "draw.text": {
        "!type": "fn(text: string, rect: ?, style?: ?)",
        "!doc": "Text laid out inside the rectangle, clipped where it does not fit.",
        "!params": [
          {
            "name": "text",
            "type": "string",
            "doc": "The string to draw.",
            "required": true
          },
          {
            "name": "rect",
            "type": "?",
            "doc": "The box to draw in.",
            "required": true,
            "fields": [
              {
                "name": "x",
                "type": "number",
                "doc": "Left edge. Default 0."
              },
              {
                "name": "y",
                "type": "number",
                "doc": "Top edge. Default 0."
              },
              {
                "name": "w",
                "type": "number",
                "doc": "Width. Default 0, which draws nothing."
              },
              {
                "name": "h",
                "type": "number",
                "doc": "Height. Default 0, which draws nothing."
              }
            ]
          },
          {
            "name": "style",
            "type": "?",
            "doc": "",
            "fields": [
              {
                "name": "font",
                "type": "+RootlessFont",
                "doc": "Size, weight and family as one value."
              },
              {
                "name": "fontSize",
                "type": "number",
                "doc": "Point size. Default 17, larger than a Text node's."
              },
              {
                "name": "fontWeight",
                "type": "FontWeight",
                "doc": "Weight, when font is not given."
              },
              {
                "name": "fontFamily",
                "type": "string",
                "doc": "Family name, when font is not given."
              },
              {
                "name": "color",
                "type": "+RootlessColor",
                "doc": "Text colour. Default white."
              },
              {
                "name": "align",
                "type": "TextAlign",
                "doc": "Alignment inside the rectangle. Default left."
              }
            ]
          }
        ]
      },
      "draw.image": {
        "!type": "fn(image: +WidgetNode, rect: ?)",
        "!doc": "Draws an image, stretched to the rectangle.",
        "!params": [
          {
            "name": "image",
            "type": "?",
            "doc": "From Image.download, Photos.image, or another canvas's render().",
            "required": true
          },
          {
            "name": "rect",
            "type": "?",
            "doc": "The box to draw in.",
            "required": true,
            "fields": [
              {
                "name": "x",
                "type": "number",
                "doc": "Left edge. Default 0."
              },
              {
                "name": "y",
                "type": "number",
                "doc": "Top edge. Default 0."
              },
              {
                "name": "w",
                "type": "number",
                "doc": "Width. Default 0, which draws nothing."
              },
              {
                "name": "h",
                "type": "number",
                "doc": "Height. Default 0, which draws nothing."
              }
            ]
          }
        ]
      }
    }
  },
  "console": {
    "!category": "systemAPIs",
    "!subtitle": "Console output for debugging",
    "!description": "Logging. Messages appear in the script console.",
    "log": {
      "!type": "fn(value: ?)",
      "!doc": "Prints to the console.",
      "!params": [
        {
          "name": "value",
          "type": "?",
          "doc": "Any number of values. Each one becomes a string and they are joined with a space. An object prints as [object Object], so pass JSON.stringify(value) to see inside one.",
          "required": true
        }
      ]
    },
    "warn": {
      "!type": "fn(value: ?)",
      "!doc": "Prints to the console as a warning.",
      "!params": [
        {
          "name": "value",
          "type": "?",
          "doc": "Any number of values. Each one becomes a string and they are joined with a space. An object prints as [object Object], so pass JSON.stringify(value) to see inside one.",
          "required": true
        }
      ]
    },
    "error": {
      "!type": "fn(value: ?)",
      "!doc": "Prints to the console as an error.",
      "!params": [
        {
          "name": "value",
          "type": "?",
          "doc": "Any number of values. Each one becomes a string and they are joined with a space. An object prints as [object Object], so pass JSON.stringify(value) to see inside one.",
          "required": true
        }
      ]
    },
    "!examples": [
      {
        "title": "Debugging a run",
        "code": "console.log(\"family: \" + Widget.currentFamily);\nconsole.warn(\"no data, falling back\");\nconsole.error(\"gave up\");\n\n// Output shows in the editor's console. Errors carry the line\n// and column they came from. Tap one to jump there."
      }
    ]
  },
  "Http": {
    "!doc": "HTTP networking API.",
    "!category": "systemAPIs",
    "!subtitle": "HTTP requests with caching",
    "!description": "Performs a request and blocks until it answers. Every method takes the same options object. cache is honoured on GET only and falls back to a stale copy when the network fails; multipart sends a form-data body, which is the only way to upload a file.",
    "!examples": [
      {
        "title": "GET with caching",
        "code": "const res = Http.get(\"https://api.example.com/data\", {\n  cache: Duration.minutes(15),\n  headers: { \"Authorization\": \"Bearer token\" }\n});\nif (res.ok) {\n  const data = res.json;\n}"
      },
      {
        "title": "POST request",
        "code": "const res = Http.post(\"https://api.example.com/submit\", {\n  body: { name: \"test\", value: 42 },\n  timeout: Duration.seconds(10)\n});\nconsole.log(res.status); // 200"
      },
      {
        "title": "PUT / PATCH / DELETE",
        "code": "Http.put(\"https://api.example.com/items/1\", {\n  body: { name: \"updated\" }\n});\nHttp.patch(\"https://api.example.com/items/1\", {\n  body: { status: \"active\" }\n});\nHttp.delete(\"https://api.example.com/items/1\");"
      },
      {
        "title": "Response properties",
        "code": "// res.ok. True if status 200-299\n// res.status. HTTP status code\n// res.text. Body as string\n// res.json. Parsed JSON or null\n// res.headers. Response headers (lowercase keys)\n// res.url. Final URL (after redirects)\n// res.mimeType. Response MIME type\n// res.error, \"timeout\" | \"network_error\" | ...\n// res.stale. True if from stale cache"
      },
      {
        "title": "Reading response headers",
        "code": "const res = Http.get(\"https://api.example.com/data\");\nconst contentType = res.headers[\"content-type\"];\nconst rateLimit = res.headers[\"x-ratelimit-remaining\"];"
      },
      {
        "title": "Uploading a file",
        "code": "Http.post(url, { multipart: [\n  { name: \"caption\", value: \"Today's chart\" },\n  { name: \"file\", base64: chart, filename: \"chart.png\", contentType: \"image/png\" },\n]});"
      }
    ],
    "get": {
      "!type": "fn(url: string, options?: ?) -> +HttpResponse",
      "!doc": "Perform an HTTP GET request.",
      "!params": [
        {
          "name": "url",
          "type": "string",
          "doc": "The URL to request.",
          "required": true
        },
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "headers",
              "type": "?",
              "doc": "Request headers as { name: value }."
            },
            {
              "name": "timeout",
              "type": "+RootlessDuration",
              "doc": "How long to wait. Also accepts a plain number of seconds."
            },
            {
              "name": "cache",
              "type": "+RootlessDuration",
              "doc": "Cache the response for this long and reuse it. GET only. A cached copy is also used when the network fails, however stale."
            }
          ]
        }
      ]
    },
    "post": {
      "!type": "fn(url: string, options?: ?) -> +HttpResponse",
      "!doc": "Perform an HTTP POST request.",
      "!params": [
        {
          "name": "url",
          "type": "string",
          "doc": "The URL to request.",
          "required": true
        },
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "headers",
              "type": "?",
              "doc": "Request headers as { name: value }."
            },
            {
              "name": "timeout",
              "type": "+RootlessDuration",
              "doc": "How long to wait. Also accepts a plain number of seconds."
            },
            {
              "name": "body",
              "type": "?",
              "doc": "The request body. An object is sent as JSON; a string is sent as-is."
            },
            {
              "name": "multipart",
              "type": "?",
              "doc": "A form-data body as { field: value }. The only way to upload a file."
            }
          ]
        }
      ]
    },
    "put": {
      "!type": "fn(url: string, options?: ?) -> +HttpResponse",
      "!doc": "Perform an HTTP PUT request.",
      "!params": [
        {
          "name": "url",
          "type": "string",
          "doc": "The URL to request.",
          "required": true
        },
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "headers",
              "type": "?",
              "doc": "Request headers as { name: value }."
            },
            {
              "name": "timeout",
              "type": "+RootlessDuration",
              "doc": "How long to wait. Also accepts a plain number of seconds."
            },
            {
              "name": "body",
              "type": "?",
              "doc": "The request body. An object is sent as JSON; a string is sent as-is."
            },
            {
              "name": "multipart",
              "type": "?",
              "doc": "A form-data body as { field: value }. The only way to upload a file."
            }
          ]
        }
      ]
    },
    "patch": {
      "!type": "fn(url: string, options?: ?) -> +HttpResponse",
      "!doc": "Perform an HTTP PATCH request.",
      "!params": [
        {
          "name": "url",
          "type": "string",
          "doc": "The URL to request.",
          "required": true
        },
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "headers",
              "type": "?",
              "doc": "Request headers as { name: value }."
            },
            {
              "name": "timeout",
              "type": "+RootlessDuration",
              "doc": "How long to wait. Also accepts a plain number of seconds."
            },
            {
              "name": "body",
              "type": "?",
              "doc": "The request body. An object is sent as JSON; a string is sent as-is."
            },
            {
              "name": "multipart",
              "type": "?",
              "doc": "A form-data body as { field: value }. The only way to upload a file."
            }
          ]
        }
      ]
    },
    "delete": {
      "!type": "fn(url: string, options?: ?) -> +HttpResponse",
      "!doc": "Perform an HTTP DELETE request.",
      "!params": [
        {
          "name": "url",
          "type": "string",
          "doc": "The URL to request.",
          "required": true
        },
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "headers",
              "type": "?",
              "doc": "Request headers as { name: value }."
            },
            {
              "name": "timeout",
              "type": "+RootlessDuration",
              "doc": "How long to wait. Also accepts a plain number of seconds."
            },
            {
              "name": "body",
              "type": "?",
              "doc": "The request body. An object is sent as JSON; a string is sent as-is."
            },
            {
              "name": "multipart",
              "type": "?",
              "doc": "A form-data body as { field: value }. The only way to upload a file."
            }
          ]
        }
      ]
    },
    "head": {
      "!type": "fn(url: string, options?: ?) -> +HttpResponse",
      "!doc": "Perform an HTTP HEAD request.",
      "!params": [
        {
          "name": "url",
          "type": "string",
          "doc": "The URL to request.",
          "required": true
        },
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "headers",
              "type": "?",
              "doc": "Request headers as { name: value }."
            },
            {
              "name": "timeout",
              "type": "+RootlessDuration",
              "doc": "How long to wait. Also accepts a plain number of seconds."
            }
          ]
        }
      ]
    },
    "request": {
      "!type": "fn(url: string, options?: ?) -> +HttpResponse",
      "!doc": "Perform a generic HTTP request. Set method via options.method.",
      "!params": [
        {
          "name": "url",
          "type": "string",
          "doc": "The URL to request.",
          "required": true
        },
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "headers",
              "type": "?",
              "doc": "Request headers as { name: value }."
            },
            {
              "name": "timeout",
              "type": "+RootlessDuration",
              "doc": "How long to wait. Also accepts a plain number of seconds."
            },
            {
              "name": "body",
              "type": "?",
              "doc": "The request body. An object is sent as JSON; a string is sent as-is."
            },
            {
              "name": "multipart",
              "type": "?",
              "doc": "A form-data body as { field: value }. The only way to upload a file."
            },
            {
              "name": "cache",
              "type": "+RootlessDuration",
              "doc": "Cache the response for this long and reuse it. GET only. A cached copy is also used when the network fails, however stale."
            },
            {
              "name": "method",
              "type": "string",
              "doc": "\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\" or \"HEAD\". Default \"GET\"."
            }
          ]
        }
      ]
    },
    "getAll": {
      "!type": "fn(urls: [string], options?: ?) -> [+HttpResponse]",
      "!doc": "Fetch several URLs at once and get the responses in the same order. Serial requests add up and a widget's time budget is short.",
      "!params": [
        {
          "name": "urls",
          "type": "[string]",
          "doc": "The URLs to fetch.",
          "required": true
        },
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "headers",
              "type": "?",
              "doc": "Request headers as { name: value }."
            },
            {
              "name": "timeout",
              "type": "+RootlessDuration",
              "doc": "How long to wait. Also accepts a plain number of seconds."
            },
            {
              "name": "cache",
              "type": "+RootlessDuration",
              "doc": "Cache the response for this long and reuse it. GET only. A cached copy is also used when the network fails, however stale."
            }
          ]
        }
      ],
      "!returns": "One response per URL, in the order given."
    }
  },
  "Storage": {
    "!doc": "Persistent key-value storage.",
    "!category": "systemAPIs",
    "!subtitle": "Persistent key-value storage",
    "!description": "Persistent key-value storage, scoped per script. Holds any JSON-serialisable value and survives between runs.",
    "!examples": [
      {
        "title": "Counting script runs",
        "code": "let count = Storage.get(\"runCount\") ?? 0;\ncount++;\nStorage.set(\"runCount\", count);\n\nText(`Run #${count}`);"
      }
    ],
    "get": {
      "!type": "fn(key: string) -> ?",
      "!doc": "Get a stored value by key. Returns null if not found."
    },
    "set": {
      "!type": "fn(key: string, value: ?)",
      "!doc": "Store a value with the given key.",
      "!params": [
        {
          "name": "key",
          "type": "string",
          "doc": "Scoped to the script that wrote it.",
          "required": true
        },
        {
          "name": "value",
          "type": "?",
          "doc": "Anything JSON can represent. A value it cannot, a Date or a function, is not stored.",
          "required": true
        }
      ]
    },
    "delete": {
      "!type": "fn(key: string)",
      "!doc": "Delete a stored value by key."
    },
    "has": {
      "!type": "fn(key: string) -> bool",
      "!doc": "Check if a key exists in storage."
    }
  },
  "Cache": {
    "!doc": "Temporary cache with expiry.",
    "!category": "systemAPIs",
    "!subtitle": "Temporary cache with TTL support",
    "!description": "Key-value storage that expires, scoped per script. Default TTL is one hour. For API responses and computed values.",
    "!examples": [
      {
        "title": "Caching API data",
        "code": "let data = Cache.get(\"weather\");\nif (!data) {\n  const res = Http.get(\"https://api.weather.com/now\");\n  data = res.json;\n  Cache.set(\"weather\", data, {\n    expiry: Duration.minutes(30)\n  });\n}"
      }
    ],
    "get": {
      "!type": "fn(key: string) -> ?",
      "!doc": "Get a cached value. Returns null if expired or missing."
    },
    "set": {
      "!type": "fn(key: string, value: ?, options?: ?)",
      "!doc": "Cache a value.",
      "!params": [
        {
          "name": "key",
          "type": "string",
          "doc": "Scoped to this script.",
          "required": true
        },
        {
          "name": "value",
          "type": "?",
          "doc": "Any JSON-serialisable value.",
          "required": true
        },
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "expiry",
              "type": "+RootlessDuration",
              "doc": "How long it stays valid. Default one hour."
            }
          ]
        }
      ]
    },
    "has": {
      "!type": "fn(key: string) -> bool",
      "!doc": "Check if a cached value exists and has not expired."
    },
    "delete": {
      "!type": "fn(key: string)",
      "!doc": "Removes the entry. Same shape as Storage.delete and Keychain.delete.",
      "!params": [
        {
          "name": "key",
          "type": "string",
          "doc": "Scoped to this script.",
          "required": true
        }
      ]
    }
  },
  "Device": {
    "!doc": "Device information and controls.",
    "!category": "systemAPIs",
    "!subtitle": "Device information, battery, screen and volume",
    "!description": "Device, battery, screen, orientation and volume, grouped under Device.info, Device.battery and Device.screen.",
    "!examples": [
      {
        "title": "Display device info",
        "code": "const info = Device.info;\nconst battery = Device.battery;\nconst screen = Device.screen;\n\nColumn({ children: [\n  Text(`${info.name} (${info.model})`),\n  Text(`${info.systemName} ${info.systemVersion}`),\n  Text(`Battery: ${Math.round(battery.level * 100)}%`),\n  Text(`Screen: ${screen.width}×${screen.height} @${screen.scale}x`),\n  Text(`Mode: ${screen.appearance}`)\n]});"
      },
      {
        "title": "Adaptive layout",
        "code": "const layout = Device.info.isPhone\n  ? Column({ children: [\n      Text(\"Phone layout\"),\n      Text(`Volume: ${Math.round(Device.volume.level * 100)}%`)\n    ]})\n  : Text(\"Tablet layout\");"
      }
    ],
    "ok": "bool",
    "info": "DeviceInfo",
    "battery": "BatteryInfo",
    "screen": "ScreenInfo",
    "volume": "VolumeInfo",
    "fonts": {
      "!type": "[string]",
      "!doc": "Font family names installed on the device, sorted. Pass one to Font.custom()."
    }
  },
  "Location": {
    "!doc": "GPS location services.",
    "!category": "systemAPIs",
    "!subtitle": "Location and geocoding services",
    "!description": "The device's coordinates, plus geocoding, distance and opening a place in Maps. Needs location access.",
    "!examples": [
      {
        "title": "Current location & address",
        "code": "const loc = Location.current({accuracy: \"100m\"});\nconst address = loc.formattedAddress();\nconsole.log(address);"
      },
      {
        "title": "Distance calculation",
        "code": "const home = Location.create(40.7128, -74.0060);\nconst office = Location.create(40.7589, -73.9851);\nconst km = home.distanceTo(office);\nconsole.log(`${km.toFixed(1)} km away`);"
      }
    ],
    "current": {
      "!type": "fn(options?: ?) -> +Coordinate",
      "!doc": "Get the current device location.",
      "!params": [
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "accuracy",
              "type": "string",
              "doc": "\"best\", \"nearest10Meters\", \"hundredMeters\", \"kilometer\" or \"threeKilometers\". Coarser is faster and uses less battery."
            }
          ]
        }
      ]
    },
    "create": {
      "!type": "fn(latitude: number, longitude: number) -> +Coordinate",
      "!doc": "Create a coordinate from latitude and longitude."
    }
  },
  "CallbackURL": {
    "!doc": "x-callback-url inter-app communication.",
    "!category": "systemAPIs",
    "!subtitle": "Inter-app communication via x-callback-url",
    "!description": "Opens another app over x-callback-url and waits for its reply. Blocks until the app calls back or 120 seconds pass.",
    "!examples": [
      {
        "title": "Run a Shortcut and get result",
        "code": "let result = CallbackURL.open(\n  \"shortcuts://x-callback-url/run-shortcut\",\n  { name: \"My Shortcut\" }\n);\nif (result.ok) {\n  console.log(result.params);\n}"
      },
      {
        "title": "Build URL without opening",
        "code": "let url = CallbackURL.build(\n  \"bear://x-callback-url/create\",\n  { title: \"Note\", text: \"Hello\" }\n);\nconsole.log(url);"
      }
    ],
    "open": {
      "!type": "fn(baseURL: string, parameters?: ?) -> +CallbackResponse",
      "!doc": "Open the target app URL with parameters, wait for callback, and return the response.",
      "!params": [
        {
          "name": "baseURL",
          "type": "string",
          "doc": "The target's x-callback-url endpoint, such as shortcuts://x-callback-url/run-shortcut.",
          "required": true
        },
        {
          "name": "parameters",
          "type": "?",
          "doc": "Query items to append. Keys and values are strings and are URL-encoded. x-source, x-success, x-error and x-cancel are added for you."
        }
      ]
    },
    "build": {
      "!type": "fn(baseURL: string, parameters?: ?) -> string",
      "!doc": "Build the full URL string with encoded parameters without opening it.",
      "!params": [
        {
          "name": "baseURL",
          "type": "string",
          "doc": "The target's x-callback-url endpoint, such as bear://x-callback-url/create.",
          "required": true
        },
        {
          "name": "parameters",
          "type": "?",
          "doc": "Query items to append, URL-encoded. No x-callback keys are added, so the target has no way to reply."
        }
      ]
    }
  },
  "Calendar": {
    "!doc": "Calendar access.",
    "!category": "systemAPIs",
    "!subtitle": "Event calendar access and manipulation",
    "!description": "The user's calendars. Fetch, create, edit and delete events, including recurrence rules. Needs calendar access.",
    "!examples": [
      {
        "title": "Fetch today's events",
        "code": "const events = Event.fetch({\n  timeframe: \"today\"\n});\nfor (const e of events) {\n  console.log(e.title, e.startDate.format(\"HH:mm\"));\n}"
      },
      {
        "title": "Create recurring event",
        "code": "const event = new Event({\n  title: \"Standup\",\n  startDate: DateTime.from({hour: 9, minute: 30}),\n  endDate: DateTime.from({hour: 9, minute: 45})\n});\nevent.setRecurrence({\n  frequency: \"weekly\", interval: 1\n});\nevent.save();"
      }
    ],
    "fetch": {
      "!type": "fn(options?: ?) -> +CalendarObject",
      "!doc": "Fetch calendars.",
      "!params": [
        {
          "name": "options",
          "type": "?",
          "doc": "Left out entirely, every calendar comes back.",
          "fields": [
            {
              "name": "default",
              "type": "bool",
              "doc": "The calendar new events go into."
            },
            {
              "name": "title",
              "type": "string",
              "doc": "Match by name."
            },
            {
              "name": "identifier",
              "type": "string",
              "doc": "Match by identifier."
            }
          ]
        }
      ],
      "!returns": "Every calendar with no options, one calendar when matching, null when nothing matches."
    },
    "presentPicker": {
      "!type": "fn(allowMultiple?: bool) -> [+CalendarObject]",
      "!doc": "Present the system calendar picker."
    }
  },
  "Event": {
    "!type": "fn(config?: ?) -> +EventObject",
    "!doc": "Creates an event. Call save() on it to write it to the calendar.",
    "!category": "systemAPIs",
    "!subtitle": "Calendar event creation and fetching",
    "!description": "A calendar event. Use the constructor to make one, Event.fetch() to query existing ones, and save()/remove() on what you get back.",
    "!examples": [
      {
        "title": "Fetch today's events",
        "code": "const events = Event.fetch({\n  timeframe: \"today\"\n});\nfor (const e of events) {\n  console.log(e.title, e.startDate.format(\"HH:mm\"));\n}"
      },
      {
        "title": "Create recurring event",
        "code": "const event = new Event({\n  title: \"Standup\",\n  startDate: DateTime.from({hour: 9, minute: 30}),\n  endDate: DateTime.from({hour: 9, minute: 45})\n});\nevent.setRecurrence({\n  frequency: \"weekly\", interval: 1\n});\nevent.save();"
      }
    ],
    "fetch": {
      "!type": "fn(query?: ?, calendars?: [+CalendarObject]) -> [+EventObject]",
      "!doc": "Events matching a query.",
      "!params": [
        {
          "name": "query",
          "type": "?",
          "doc": "",
          "required": true,
          "fields": [
            {
              "name": "timeframe",
              "type": "string",
              "doc": "today, tomorrow, thisWeek, nextWeek, thisMonth. Fills from and to for you."
            },
            {
              "name": "from",
              "type": "+RootlessDateTime",
              "doc": "Start of the range, when timeframe is not given."
            },
            {
              "name": "to",
              "type": "+RootlessDateTime",
              "doc": "End of the range."
            },
            {
              "name": "limit",
              "type": "number",
              "doc": "Stop after this many."
            }
          ]
        },
        {
          "name": "calendars",
          "type": "[?]",
          "doc": "Only search these. Every calendar when left out."
        }
      ]
    },
    "presentCreate": {
      "!type": "fn() -> +EventObject",
      "!doc": "Present the system event creation dialog."
    },
    "!params": [
      {
        "name": "config",
        "type": "?",
        "doc": "",
        "fields": [
          {
            "name": "title",
            "type": "string",
            "doc": "What it is called.",
            "required": true
          },
          {
            "name": "startDate",
            "type": "+RootlessDateTime",
            "doc": "When it starts.",
            "required": true
          },
          {
            "name": "endDate",
            "type": "+RootlessDateTime",
            "doc": "When it ends."
          },
          {
            "name": "isAllDay",
            "type": "bool",
            "doc": "An all-day event ignores the times."
          },
          {
            "name": "location",
            "type": "string",
            "doc": "Where it is."
          },
          {
            "name": "notes",
            "type": "string",
            "doc": "Free text."
          },
          {
            "name": "calendar",
            "type": "?",
            "doc": "Which calendar to write to. The default one when left out."
          },
          {
            "name": "availability",
            "type": "string",
            "doc": "busy, free, tentative or unavailable."
          }
        ]
      }
    ]
  },
  "Notification": {
    "!doc": "Local notification scheduling.",
    "!category": "systemAPIs",
    "!subtitle": "Local notification scheduling",
    "!description": "Schedules a local notification with a title, body, sound and interactive actions. Fires after a delay or at a date.",
    "!examples": [
      {
        "title": "Schedule a reminder",
        "code": "Notification.schedule({\n  id: \"reminder\",\n  title: \"Time's up!\",\n  body: \"Your timer has finished.\",\n  sound: \"complete\",\n  delay: Duration.minutes(5)\n});"
      }
    ],
    "schedule": {
      "!type": "fn(options: ?) -> string",
      "!doc": "Schedules a local notification and returns the id it used, so an auto-generated one can still be cancelled. Throws if title is missing.",
      "!params": [
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "title",
              "type": "string",
              "doc": "The heading.",
              "required": true
            },
            {
              "name": "body",
              "type": "string",
              "doc": "The line under it."
            },
            {
              "name": "subtitle",
              "type": "string",
              "doc": "A second heading between title and body."
            },
            {
              "name": "id",
              "type": "string",
              "doc": "Your own identifier, for cancelling it later. Generated if absent."
            },
            {
              "name": "sound",
              "type": "bool",
              "doc": "Play the default sound."
            },
            {
              "name": "badge",
              "type": "number",
              "doc": "Set the app icon badge."
            },
            {
              "name": "delay",
              "type": "+RootlessDuration",
              "doc": "Fire after this long. Minimum one second."
            },
            {
              "name": "date",
              "type": "+RootlessDateTime",
              "doc": "Fire at this moment. Ignored when delay is given."
            },
            {
              "name": "actions",
              "type": "[?]",
              "doc": "Buttons, as [{ id, title }]. Tapping one runs the script with that id in Script.actions."
            }
          ]
        }
      ]
    },
    "cancel": {
      "!type": "fn(id: string)",
      "!doc": "Cancel a scheduled notification by ID."
    },
    "cancelAll": {
      "!type": "fn()",
      "!doc": "Cancel all scheduled notifications for this script."
    }
  },
  "FileSystem": {
    "!doc": "File system access.",
    "!category": "systemAPIs",
    "!subtitle": "File and directory access",
    "!description": "The app's local storage and iCloud. Navigate directories, read and write files, and keep bookmarks to folders outside the app. Handles text, JSON, images and binary data.",
    "!examples": [
      {
        "title": "Read & write files",
        "code": "const dir = FileSystem.local.documents;\nconst file = dir.file(\"data.json\");\n\nfile.write(JSON.stringify({count: 1}));\nconst data = file.read(\"json\");\nconsole.log(data.count); // 1"
      },
      {
        "title": "File properties",
        "code": "// file.path, file.name, file.extension\n// file.exists, file.size (bytes)\n// file.createdAt, file.modifiedAt (DateTime)"
      }
    ],
    "local": {
      "!type": "+DirectoryObject",
      "!doc": "The app's local storage directory."
    },
    "iCloud": {
      "!type": "+DirectoryObject",
      "!doc": "The app's iCloud container directory. May be null if iCloud is unavailable."
    },
    "bookmarks": {
      "!type": "fn(name: string) -> ?",
      "!doc": "Resolve a saved security-scoped bookmark by name. Returns a Directory or File."
    },
    "saveBookmark": {
      "!type": "fn(name: string, path: string)",
      "!doc": "Save a security-scoped bookmark."
    }
  },
  "DateTime": {
    "!doc": "Date and time utilities.",
    "!category": "primitives",
    "!subtitle": "Date and time creation, parsing, and manipulation",
    "!description": "Dates and times. Static methods make one, instance methods compare, format and do arithmetic.",
    "!examples": [
      {
        "title": "Current date & formatting",
        "code": "const now = DateTime.now();\nconst formatted = now.format(\"MMM d, yyyy\");\n// \"Mar 13, 2026\""
      },
      {
        "title": "Parsing & comparison",
        "code": "const date = DateTime.parse(\"2026-01-01\");\nconst today = DateTime.today();\nconsole.log(date.isBefore(today)); // true"
      },
      {
        "title": "Date arithmetic",
        "code": "const tomorrow = DateTime.now()\n  .add(Duration.days(1));\nconst diff = tomorrow.difference(DateTime.now());\nconsole.log(diff.inHours); // ~24"
      }
    ],
    "now": {
      "!type": "fn() -> +RootlessDateTime",
      "!doc": "Get the current date and time."
    },
    "today": {
      "!type": "fn() -> +RootlessDateTime",
      "!doc": "Get today's date at midnight."
    },
    "parse": {
      "!type": "fn(isoString: string) -> +RootlessDateTime",
      "!doc": "Parse an ISO 8601 date string."
    },
    "from": {
      "!type": "fn(options: ?) -> +RootlessDateTime",
      "!doc": "Create a date from components: { year, month, day, hour, minute, second }.",
      "!params": [
        {
          "name": "options",
          "type": "?",
          "doc": "Anything left out defaults to the current value for that component.",
          "fields": [
            {
              "name": "year",
              "type": "number",
              "doc": "Defaults to the current year."
            },
            {
              "name": "month",
              "type": "number",
              "doc": "1 to 12."
            },
            {
              "name": "day",
              "type": "number",
              "doc": "1 to 31."
            },
            {
              "name": "hour",
              "type": "number",
              "doc": "0 to 23."
            },
            {
              "name": "minute",
              "type": "number",
              "doc": "0 to 59."
            },
            {
              "name": "second",
              "type": "number",
              "doc": "0 to 59."
            }
          ]
        }
      ]
    }
  },
  "Duration": {
    "!doc": "Time duration utilities.",
    "!category": "primitives",
    "!subtitle": "Time duration representation",
    "!description": "A span of time. Static methods build one, then read it back in any unit or combine it with another.",
    "!examples": [
      {
        "title": "Creating durations",
        "code": "const oneHour = Duration.hours(1);\nconst halfDay = Duration.hours(12);\nconst total = oneHour.add(halfDay);\nconsole.log(total.inHours); // 13"
      }
    ],
    "seconds": {
      "!type": "fn(n: number) -> +RootlessDuration",
      "!doc": "Create a duration of n seconds."
    },
    "minutes": {
      "!type": "fn(n: number) -> +RootlessDuration",
      "!doc": "Create a duration of n minutes."
    },
    "hours": {
      "!type": "fn(n: number) -> +RootlessDuration",
      "!doc": "Create a duration of n hours."
    },
    "days": {
      "!type": "fn(n: number) -> +RootlessDuration",
      "!doc": "Create a duration of n days."
    }
  },
  "Color": {
    "!doc": "Color creation utilities.",
    "!category": "primitives",
    "!subtitle": "Color creation and manipulation",
    "!description": "Colours from hex, RGB, RGBA or HSL, plus named and semantic system colours. Instance methods adjust opacity and brightness.",
    "!examples": [
      {
        "title": "Named & hex colors",
        "code": "const red = Color.red();\nconst custom = Color.hex(\"#1a73e8\");\nconst semi = custom.opacity(0.5);"
      },
      {
        "title": "Predefined colors",
        "code": "// Named: white, black, gray, red, blue,\n// green, orange, yellow, purple, pink,\n// teal, mint, cyan, brown, indigo, clear\n//\n// Semantic: label, secondaryLabel,\n// tertiaryLabel, systemBackground,\n// secondaryBackground, separator, systemFill"
      },
      {
        "title": "RGB & HSL",
        "code": "const c1 = Color.rgb(26, 115, 232);\nconst c2 = Color.hsl(217, 80, 50);\nconst dark = c1.darker(0.2);"
      }
    ],
    "hex": {
      "!type": "fn(hex: string) -> +RootlessColor",
      "!doc": "Create a color from a hex string (e.g. '#ff0000')."
    },
    "rgb": {
      "!type": "fn(r: number, g: number, b: number) -> +RootlessColor",
      "!doc": "Create a color from RGB values (0-255)."
    },
    "rgba": {
      "!type": "fn(r: number, g: number, b: number, a: number) -> +RootlessColor",
      "!doc": "Create a color from RGBA values (RGB: 0-255, A: 0-1)."
    },
    "hsl": {
      "!type": "fn(h: number, s: number, l: number) -> +RootlessColor",
      "!doc": "Create a color from HSL values (H: 0-360, S: 0-100, L: 0-100)."
    },
    "white": {
      "!type": "fn() -> +RootlessColor",
      "!doc": "White color."
    },
    "black": {
      "!type": "fn() -> +RootlessColor",
      "!doc": "Black color."
    },
    "gray": {
      "!type": "fn() -> +RootlessColor",
      "!doc": "Gray color."
    },
    "red": {
      "!type": "fn() -> +RootlessColor",
      "!doc": "Red color."
    },
    "blue": {
      "!type": "fn() -> +RootlessColor",
      "!doc": "Blue color."
    },
    "green": {
      "!type": "fn() -> +RootlessColor",
      "!doc": "Green color."
    },
    "orange": {
      "!type": "fn() -> +RootlessColor",
      "!doc": "Orange color."
    },
    "yellow": {
      "!type": "fn() -> +RootlessColor",
      "!doc": "Yellow color."
    },
    "purple": {
      "!type": "fn() -> +RootlessColor",
      "!doc": "Purple color."
    },
    "pink": {
      "!type": "fn() -> +RootlessColor",
      "!doc": "Pink color."
    },
    "teal": {
      "!type": "fn() -> +RootlessColor",
      "!doc": "Teal color."
    },
    "mint": {
      "!type": "fn() -> +RootlessColor",
      "!doc": "Mint color."
    },
    "cyan": {
      "!type": "fn() -> +RootlessColor",
      "!doc": "Cyan color."
    },
    "brown": {
      "!type": "fn() -> +RootlessColor",
      "!doc": "Brown color."
    },
    "indigo": {
      "!type": "fn() -> +RootlessColor",
      "!doc": "Indigo color."
    },
    "clear": {
      "!type": "fn() -> +RootlessColor",
      "!doc": "Transparent color."
    },
    "label": {
      "!type": "fn() -> +RootlessColor",
      "!doc": "System label color (adapts to light/dark mode)."
    },
    "secondaryLabel": {
      "!type": "fn() -> +RootlessColor",
      "!doc": "System secondary label color."
    },
    "tertiaryLabel": {
      "!type": "fn() -> +RootlessColor",
      "!doc": "System tertiary label color."
    },
    "systemBackground": {
      "!type": "fn() -> +RootlessColor",
      "!doc": "System background color."
    },
    "secondaryBackground": {
      "!type": "fn() -> +RootlessColor",
      "!doc": "System secondary background color."
    },
    "separator": {
      "!type": "fn() -> +RootlessColor",
      "!doc": "System separator color."
    },
    "systemFill": {
      "!type": "fn() -> +RootlessColor",
      "!doc": "System fill color."
    }
  },
  "Size": {
    "!doc": "Size specification utilities.",
    "!category": "primitives",
    "!subtitle": "Fixed, infinite, or fractional sizing",
    "!description": "A dimension. fixed for an exact value, infinity for all the space available, fraction for a proportion of it.",
    "!examples": [
      {
        "title": "Using sizes",
        "code": "Container({\n  width: Size.fixed(200),\n  height: Size.fraction(0.5),\n  child: Text(\"Half height\")\n})"
      }
    ],
    "fixed": {
      "!type": "fn(value: number) -> +RootlessSize",
      "!doc": "Create a fixed pixel size."
    },
    "fraction": {
      "!type": "fn(value: number) -> +RootlessSize",
      "!doc": "Create a fractional size (0-1) of available space."
    },
    "infinity": {
      "!type": "+RootlessSize",
      "!doc": "Infinite/maximum available size."
    }
  },
  "EdgeInsets": {
    "!doc": "Edge insets for padding and margins.",
    "!category": "primitives",
    "!subtitle": "Padding and margin specification",
    "!description": "Padding and margins. Uniform, symmetric, or one side at a time.",
    "!examples": [
      {
        "title": "Padding examples",
        "code": "const pad = EdgeInsets.all(16);\nconst hv = EdgeInsets.symmetric({\n  horizontal: 20, vertical: 10\n});\nconst custom = EdgeInsets.only({\n  top: 8, bottom: 16\n});"
      }
    ],
    "all": {
      "!type": "fn(value: number) -> +EdgeInsetsObject",
      "!doc": "Same inset on all sides."
    },
    "symmetric": {
      "!type": "fn(horizontal?: number, vertical?: number) -> +EdgeInsetsObject",
      "!doc": "Symmetric horizontal and vertical insets."
    },
    "only": {
      "!type": "fn(options: ?) -> +EdgeInsetsObject",
      "!doc": "Individual side insets: { top, leading, bottom, trailing }. left and right are accepted too.",
      "!params": [
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "top",
              "type": "number",
              "doc": "Points."
            },
            {
              "name": "leading",
              "type": "number",
              "doc": "Points. `left` also works."
            },
            {
              "name": "bottom",
              "type": "number",
              "doc": "Points."
            },
            {
              "name": "trailing",
              "type": "number",
              "doc": "Points. `right` also works."
            }
          ]
        }
      ]
    },
    "zero": {
      "!type": "fn() -> +EdgeInsetsObject",
      "!doc": "Zero insets on all sides."
    }
  },
  "Alignment": {
    "!doc": "Alignment constants.",
    "!category": "primitives",
    "!subtitle": "Positioning constants for widget alignment",
    "!description": "Named positions for a child inside a Container or a Stack. Corner and edge names (topLeft, centerRight) both work, as do the short edge names top, bottom, leading and trailing.",
    "!examples": [
      {
        "title": "Alignment values",
        "code": "// Available alignments:\n// topLeft, topCenter, topRight\n// centerLeft, center, centerRight\n// bottomLeft, bottomCenter, bottomRight\n\nContainer({\n  alignment: Alignment.bottomRight,\n  child: Text(\"Bottom right\")\n})"
      }
    ],
    "topLeft": "+AlignmentObject",
    "topCenter": "+AlignmentObject",
    "topRight": "+AlignmentObject",
    "centerLeft": "+AlignmentObject",
    "center": "+AlignmentObject",
    "centerRight": "+AlignmentObject",
    "bottomLeft": "+AlignmentObject",
    "bottomCenter": "+AlignmentObject",
    "bottomRight": "+AlignmentObject",
    "top": "string",
    "bottom": "string",
    "leading": "string",
    "trailing": "string"
  },
  "Script": {
    "!doc": "Script execution context.",
    "!category": "systemAPIs",
    "!subtitle": "Entry point for widget rendering and Shortcuts integration",
    "!description": "The entry point. Script.setWidget() registers the root widget; Script.return() passes a value back to Shortcuts.",
    "!examples": [
      {
        "title": "Basic widget script",
        "code": "const widget = new Widget({\n  child: Text(\"Hello, World!\")\n});\nScript.setWidget(widget);"
      },
      {
        "title": "Shortcuts integration",
        "code": "// Receive input from Shortcuts via Widget.parameter\nconst input = Number(Widget.parameter) || 0;\nconst result = input * 2;\n\n// Return the result to Shortcuts\nScript.return(result);"
      }
    ],
    "input": {
      "!type": "?",
      "!doc": "What was handed to this run, or null when nothing was.",
      "!returns": "{ text, url, urls, images, source }. text is the shared text or the page title; url is the first link; images are ready for an Image node; source names the app it came from."
    },
    "setWidget": {
      "!type": "fn(widget: +WidgetNode)",
      "!doc": "Set the widget to be rendered."
    },
    "return": {
      "!type": "fn(value: ?)",
      "!doc": "Set the return value for headless script execution.",
      "!params": [
        {
          "name": "value",
          "type": "?",
          "doc": "Anything JSON can represent. A headless run hands it back to whatever started the script.",
          "required": true
        }
      ]
    },
    "actions": {
      "!type": "[?]",
      "!doc": "Actions queued since the last run, oldest first.",
      "!returns": "One { name, payload } per action. name is what you passed to a Button or Toggle; payload is whatever came with it, and a Toggle puts its new state under value."
    },
    "onAction": {
      "!type": "fn(handler: fn(action: ?))",
      "!doc": "Runs the handler once for every action in Script.actions, in order. Call it before you build your widget.",
      "!params": [
        {
          "name": "handler",
          "type": "fn(action: ?)",
          "doc": "Called once for every queued action, in order. It receives the action:",
          "required": true,
          "fields": [
            {
              "name": "name",
              "type": "string",
              "doc": "The action string set on the Button or Toggle that was tapped."
            },
            {
              "name": "payload",
              "type": "?",
              "doc": "Whatever that component attached to it, or null."
            }
          ]
        }
      ]
    }
  },
  "Widget": {
    "!type": "fn(config?: ?) -> +WidgetNode",
    "!doc": "The root of the widget tree.",
    "!category": "uiComponents",
    "!subtitle": "Root widget container",
    "!description": "The root of the widget tree. Build one with new Widget({...}) and hand it to Script.setWidget(). Widget.currentFamily and Widget.parameter let the layout adapt.\n\nThe tap action is configured on the Home Screen, not here. A Link node takes priority inside its own area; tapping anywhere else runs the configured action.",
    "!examples": [
      {
        "title": "Adaptive widget",
        "code": "const family = Widget.currentFamily;\nconst isSmall = family === \"small\";\n\nconst content = isSmall\n  ? Text(\"Compact\")\n  : Column({ children: [\n      Text(\"Expanded\"),\n      Text(\"More details here\")\n    ]});\n\nconst widget = new Widget({\n  backgroundColor: Color.hex(\"#1a1a2e\"),\n  padding: EdgeInsets.all(16),\n  child: content\n});\nScript.setWidget(widget);"
      }
    ],
    "currentFamily": {
      "!type": "string",
      "!doc": "The current widget family: 'small', 'medium', 'large', etc.",
      "!doctype": "WidgetFamily"
    },
    "parameter": {
      "!type": "string",
      "!doc": "The text typed into the widget's Parameter field, or passed as Parameter from Shortcuts. One free-text value per widget instance, so the same script on three widgets can show three different things. Null when unset. From Shortcuts this is the same string as Script.input.text."
    },
    "requestReload": {
      "!type": "fn()",
      "!doc": "Request a widget timeline reload. Tells WidgetKit to re-run the script and refresh the widget display."
    },
    "size": {
      "!type": "?",
      "!doc": "The widget's real point size.",
      "!returns": "{ width, height }. Medium is 364x170 only on the widest phones; 338x158 and 329x155 are more common, so a layout against fixed numbers overflows elsewhere."
    },
    "!params": [
      {
        "name": "config",
        "type": "?",
        "doc": "",
        "fields": [
          {
            "name": "child",
            "type": "+WidgetNode",
            "doc": "What the widget draws."
          },
          {
            "name": "supportedFamilies",
            "type": "[WidgetFamily]",
            "doc": "Which sizes this script offers. Every family by default."
          },
          {
            "name": "refreshInterval",
            "type": "+RootlessDuration",
            "doc": "How often to ask for a redraw. iOS decides whether to grant it; five minutes is the floor."
          },
          {
            "name": "backgroundColor",
            "type": "+RootlessColor",
            "doc": "A flat background."
          },
          {
            "name": "backgroundGradient",
            "type": "+LinearGradient",
            "doc": "A gradient background. Wins over backgroundColor."
          },
          {
            "name": "padding",
            "type": "+EdgeInsets",
            "doc": "Space between the edge and the child."
          }
        ]
      }
    ]
  },
  "Text": {
    "!type": "fn(content: string, options?: ?) -> +WidgetNode",
    "!doc": "Renders a string.",
    "!category": "uiComponents",
    "!subtitle": "Display text with styling options",
    "!description": "Renders a string. Set type with font; see Font for the named styles and Font.custom.",
    "!examples": [
      {
        "title": "Styled text",
        "code": "Text(\"Hello World\", {\n  color: Color.white(),\n  font: Font.system(24, FontWeight.bold),\n  textAlign: \"center\"\n})"
      }
    ],
    "timer": {
      "!type": "fn(endsAt: ?, options?: ?) -> +WidgetNode",
      "!doc": "A clock that keeps counting without a timeline reload. Counts down to `endsAt` (epoch seconds, a DateTime, or a Date), or up once it has passed. Use this for anything live, a widget only redraws every few minutes.",
      "!params": [
        {
          "name": "endsAt",
          "type": "?",
          "doc": "A DateTime, a Date, or epoch seconds. Counts down to it, and up once it has passed.",
          "required": true
        },
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "font",
              "type": "+RootlessFont",
              "doc": "Size, weight, family and design as one value."
            },
            {
              "name": "color",
              "type": "+RootlessColor",
              "doc": "Text colour."
            },
            {
              "name": "textAlign",
              "type": "TextAlign",
              "doc": "A TextAlign, for alignment inside the available width."
            },
            {
              "name": "maxLines",
              "type": "number",
              "doc": "Truncate beyond this many lines."
            },
            {
              "name": "overflow",
              "type": "TextOverflow",
              "doc": "A TextOverflow, for how truncation looks. Needs maxLines."
            },
            {
              "name": "lineSpacing",
              "type": "number",
              "doc": "Extra points between lines."
            }
          ]
        }
      ]
    },
    "measure": {
      "!type": "fn(text: string, options?: ?) -> ?",
      "!doc": "How big this string will be. Pair with Widget.size rather than guessing whether text fits.",
      "!params": [
        {
          "name": "text",
          "type": "string",
          "doc": "The string to measure.",
          "required": true
        },
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "font",
              "type": "+RootlessFont",
              "doc": "The font it will be drawn in. Left out, the system font at 14 points."
            },
            {
              "name": "maxWidth",
              "type": "number",
              "doc": "The width it has to fit in."
            },
            {
              "name": "maxLines",
              "type": "number",
              "doc": "The line budget, for `fits`."
            }
          ]
        }
      ],
      "!returns": "{ width, height, lines, lineHeight, fits }. fits is false when the string needs more lines than maxLines allows."
    },
    "!params": [
      {
        "name": "content",
        "type": "string",
        "doc": "The string to render.",
        "required": true
      },
      {
        "name": "options",
        "type": "?",
        "doc": "",
        "fields": [
          {
            "name": "font",
            "type": "+RootlessFont",
            "doc": "Size, weight, family and design as one value."
          },
          {
            "name": "color",
            "type": "+RootlessColor",
            "doc": "Text colour."
          },
          {
            "name": "textAlign",
            "type": "TextAlign",
            "doc": "A TextAlign, for alignment inside the available width."
          },
          {
            "name": "maxLines",
            "type": "number",
            "doc": "Truncate beyond this many lines."
          },
          {
            "name": "overflow",
            "type": "TextOverflow",
            "doc": "A TextOverflow, for how truncation looks. Needs maxLines."
          },
          {
            "name": "lineSpacing",
            "type": "number",
            "doc": "Extra points between lines."
          }
        ]
      }
    ]
  },
  "Column": {
    "!type": "fn(options?: ?) -> +WidgetNode",
    "!doc": "Vertical layout container.",
    "!category": "uiComponents",
    "!subtitle": "Vertical layout container",
    "!description": "Lays its children out vertically. children is a plain array.\n\nA Column fills its main axis, so mainAxisAlignment has space to work with, and hugs its content horizontally. crossAxisAlignment: \"stretch\" fills that direction too.",
    "!examples": [
      {
        "title": "Building children up",
        "code": "var rows = [];\nfor (var i = 0; i < 3; i++) {\n  rows.push(Text(\"Item \" + i, { font: Font.system(14) }));\n  rows.push(SizedBox({ height: 4 }));\n}\n\nColumn({\n  mainAxisAlignment: MainAxisAlignment.start,\n  crossAxisAlignment: CrossAxisAlignment.start,\n  children: rows,\n})"
      }
    ],
    "!params": [
      {
        "name": "options",
        "type": "?",
        "doc": "",
        "fields": [
          {
            "name": "mainAxisAlignment",
            "type": "MainAxisAlignment",
            "doc": "Along the vertical. Default start."
          },
          {
            "name": "crossAxisAlignment",
            "type": "CrossAxisAlignment",
            "doc": "Across it. Default start."
          },
          {
            "name": "children",
            "type": "[+WidgetNode]",
            "doc": "A plain array. Push to it, map into it, spread it.",
            "required": true
          }
        ]
      }
    ]
  },
  "Row": {
    "!type": "fn(options?: ?) -> +WidgetNode",
    "!doc": "Horizontal layout container.",
    "!category": "uiComponents",
    "!subtitle": "Horizontal layout container",
    "!description": "Lays its children out horizontally. children is a plain array.\n\nA Row fills its main axis, so mainAxisAlignment has space to work with, and hugs its content vertically. crossAxisAlignment: \"stretch\" fills that direction too.",
    "!examples": [
      {
        "title": "Spaced row",
        "code": "Row({\n  mainAxisAlignment: \"spaceBetween\",\n  children: [\n    Text(\"Left\"),\n    Text(\"Right\")\n  ]\n})"
      }
    ],
    "!params": [
      {
        "name": "options",
        "type": "?",
        "doc": "",
        "fields": [
          {
            "name": "mainAxisAlignment",
            "type": "MainAxisAlignment",
            "doc": "Along the horizontal. Default start."
          },
          {
            "name": "crossAxisAlignment",
            "type": "CrossAxisAlignment",
            "doc": "Across it. Default start."
          },
          {
            "name": "children",
            "type": "[+WidgetNode]",
            "doc": "A plain array. Push to it, map into it, spread it.",
            "required": true
          }
        ]
      }
    ]
  },
  "Container": {
    "!type": "fn(options?: ?) -> +WidgetNode",
    "!doc": "A container with styling.",
    "!category": "uiComponents",
    "!subtitle": "Box container with styling",
    "!description": "A box: background colour or gradient, padding, margin, corner radius, alignment and explicit size.",
    "!examples": [
      {
        "title": "Styled card",
        "code": "Container({\n  backgroundColor: Color.hex(\"#1e1e1e\"),\n  padding: EdgeInsets.all(16),\n  borderRadius: 12,\n  child: Text(\"Card content\", {\n    color: Color.white()\n  })\n})"
      }
    ],
    "!params": [
      {
        "name": "options",
        "type": "?",
        "doc": "",
        "fields": [
          {
            "name": "backgroundColor",
            "type": "+RootlessColor",
            "doc": "A flat fill."
          },
          {
            "name": "backgroundGradient",
            "type": "+LinearGradient",
            "doc": "A gradient fill. Wins over backgroundColor."
          },
          {
            "name": "padding",
            "type": "+EdgeInsets",
            "doc": "Space inside the border."
          },
          {
            "name": "margin",
            "type": "+EdgeInsets",
            "doc": "Space outside it."
          },
          {
            "name": "borderRadius",
            "type": "number",
            "doc": "Rounds all four corners."
          },
          {
            "name": "alignment",
            "type": "Alignment",
            "doc": "Where the child sits when the box is larger than it."
          },
          {
            "name": "width",
            "type": "?",
            "doc": "A number of points, or a Size."
          },
          {
            "name": "height",
            "type": "?",
            "doc": "A number of points, or a Size."
          },
          {
            "name": "child",
            "type": "+WidgetNode",
            "doc": "The node inside."
          }
        ]
      }
    ]
  },
  "Stack": {
    "!type": "fn(options?: ?) -> +WidgetNode",
    "!doc": "Z-axis stacking container.",
    "!category": "uiComponents",
    "!subtitle": "Overlapping layout (z-order)",
    "!description": "Layers its children, later ones in front. alignment positions them all at once.",
    "!examples": [
      {
        "title": "Overlay badge",
        "code": "Stack({\n  alignment: Alignment.topRight,\n  children: [\n    new Image.symbol(\"bell.fill\", {fontSize: 32}),\n    Container({\n      backgroundColor: Color.red(),\n      borderRadius: 8,\n      padding: EdgeInsets.all(4),\n      child: Text(\"3\", {\n        color: Color.white(), font: Font.system(10)\n      })\n    })\n  ]\n})"
      }
    ],
    "!params": [
      {
        "name": "options",
        "type": "?",
        "doc": "",
        "fields": [
          {
            "name": "alignment",
            "type": "Alignment",
            "doc": "Where every child sits. Default center."
          },
          {
            "name": "children",
            "type": "[+WidgetNode]",
            "doc": "A plain array. Push to it, map into it, spread it.",
            "required": true
          }
        ]
      }
    ]
  },
  "SizedBox": {
    "!type": "fn(options?: ?) -> +WidgetNode",
    "!doc": "A box with fixed dimensions.",
    "!category": "uiComponents",
    "!subtitle": "Fixed or constrained size box",
    "!description": "Forces its child to a width and/or height. Also used as a fixed gap between widgets.",
    "!examples": [
      {
        "title": "Vertical spacing",
        "code": "Column({ children: [\n  Text(\"Above\"),\n  SizedBox({height: Size.fixed(16)}),\n  Text(\"Below\")\n]})"
      }
    ],
    "!params": [
      {
        "name": "options",
        "type": "?",
        "doc": "",
        "fields": [
          {
            "name": "width",
            "type": "?",
            "doc": "A number of points, or a Size."
          },
          {
            "name": "height",
            "type": "?",
            "doc": "A number of points, or a Size."
          },
          {
            "name": "child",
            "type": "+WidgetNode",
            "doc": "The node inside."
          }
        ]
      }
    ]
  },
  "Spacer": {
    "!type": "fn(options?: ?) -> +WidgetNode",
    "!doc": "A flexible spacer.",
    "!category": "uiComponents",
    "!subtitle": "Flexible space filler",
    "!description": "Takes the free space along the main axis, pushing what is on either side of it apart. Spacers in the same stack divide that space between them, in proportion to their flex.",
    "!examples": [
      {
        "title": "Push to edges",
        "code": "Row({ children: [\n  Text(\"Left\"),\n  Spacer(),\n  Text(\"Right\")\n]})"
      }
    ],
    "!params": [
      {
        "name": "options",
        "type": "?",
        "doc": "",
        "fields": [
          {
            "name": "flex",
            "type": "number",
            "doc": "How many shares of the free space this spacer takes, against the other spacers in the same stack. Default 1."
          }
        ]
      }
    ]
  },
  "Divider": {
    "!type": "fn(options?: ?) -> +WidgetNode",
    "!doc": "A visual divider line.",
    "!category": "uiComponents",
    "!subtitle": "Visual separator line",
    "!description": "A line between things. Horizontal by default; set width for a vertical one, or both width and height for a fixed size.",
    "!examples": [
      {
        "title": "Horizontal separator",
        "code": "Column({ children: [\n  Text(\"Section 1\"),\n  Divider({ color: Color.gray().opacity(0.3) }),\n  Text(\"Section 2\")\n]})"
      },
      {
        "title": "Vertical divider in a row",
        "code": "Row({ children: [\n  Text(\"Left\"),\n  Divider({ width: 1, height: 20, color: Color.gray() }),\n  Text(\"Right\")\n]})"
      }
    ],
    "!params": [
      {
        "name": "options",
        "type": "?",
        "doc": "",
        "fields": [
          {
            "name": "color",
            "type": "+RootlessColor",
            "doc": "Line colour."
          },
          {
            "name": "thickness",
            "type": "number",
            "doc": "How thick the line is."
          },
          {
            "name": "width",
            "type": "number",
            "doc": "Set it to draw a vertical divider instead."
          },
          {
            "name": "height",
            "type": "number",
            "doc": "With width, gives a fixed-size separator."
          },
          {
            "name": "indent",
            "type": "number",
            "doc": "Inset at the start."
          },
          {
            "name": "endIndent",
            "type": "number",
            "doc": "Inset at the end."
          }
        ]
      }
    ]
  },
  "Button": {
    "!type": "fn(options?: ?) -> +WidgetNode",
    "!doc": "A tappable button.",
    "!category": "uiComponents",
    "!subtitle": "Tappable button",
    "!description": "A tappable element. Pass child to draw it yourself, or text for a plain label. action is a name your script recognises: the tap re-runs the script with that action in Script.actions, in the widget and in the preview.",
    "!examples": [
      {
        "title": "Counter button",
        "code": "var count = Storage.get(\"count\") || 0;\n\nScript.onAction(function (a) {\n  if (a.name === \"increment\") count += a.payload.by;\n});\nStorage.set(\"count\", count);\n\nScript.setWidget(new Widget({\n  child: Row({ children: [\n    Text(String(count)),\n    Button({ text: \"+1\", action: \"increment\", payload: { by: 1 } }),\n  ]}),\n}));"
      }
    ],
    "!params": [
      {
        "name": "options",
        "type": "?",
        "doc": "",
        "fields": [
          {
            "name": "text",
            "type": "string",
            "doc": "A plain label. Ignored when child is given."
          },
          {
            "name": "color",
            "type": "+RootlessColor",
            "doc": "Label colour."
          },
          {
            "name": "padding",
            "type": "+EdgeInsets",
            "doc": "Space around the label."
          },
          {
            "name": "child",
            "type": "+WidgetNode",
            "doc": "Draw the button yourself. Wins over text."
          },
          {
            "name": "action",
            "type": "string",
            "doc": "A name your script recognises. The tap re-runs the script with it in Script.actions."
          },
          {
            "name": "payload",
            "type": "?",
            "doc": "Any JSON value, delivered alongside the action."
          }
        ]
      }
    ]
  },
  "Toggle": {
    "!type": "fn(options?: ?) -> +WidgetNode",
    "!doc": "A toggle switch.",
    "!category": "uiComponents",
    "!subtitle": "Switch or checkbox control",
    "!description": "A switch or checkbox. action is a name your script recognises: flipping it re-runs the script with that action in Script.actions, and the payload carries the new state under value.",
    "!examples": [
      {
        "title": "Settings toggle",
        "code": "var on = Storage.get(\"notifs\") || false;\n\nScript.onAction(function (a) {\n  if (a.name === \"setNotifs\") on = a.payload.value;\n});\nStorage.set(\"notifs\", on);\n\nScript.setWidget(new Widget({\n  child: Toggle({\n    value: on,\n    label: \"Notifications\",\n    color: Color.green(),\n    action: \"setNotifs\",\n  }),\n}));"
      }
    ],
    "!params": [
      {
        "name": "options",
        "type": "?",
        "doc": "",
        "fields": [
          {
            "name": "isOn",
            "type": "bool",
            "doc": "The current state.",
            "required": true
          },
          {
            "name": "label",
            "type": "string",
            "doc": "Text beside it."
          },
          {
            "name": "color",
            "type": "+RootlessColor",
            "doc": "Tint when on."
          },
          {
            "name": "style",
            "type": "ToggleStyle",
            "doc": "switch or checkbox. Default switch."
          },
          {
            "name": "action",
            "type": "string",
            "doc": "A name your script recognises. The tap re-runs the script with it in Script.actions."
          },
          {
            "name": "payload",
            "type": "?",
            "doc": "Any JSON value, delivered alongside the action."
          }
        ]
      }
    ]
  },
  "Link": {
    "!type": "fn(options?: ?) -> +WidgetNode",
    "!doc": "A deep link wrapper.",
    "!category": "uiComponents",
    "!subtitle": "Tappable link to URL",
    "!description": "Wraps a child and opens a URL when tapped. Inside a widget, a Link takes priority over the tap action configured on the Home Screen.",
    "!examples": [
      {
        "title": "Web link",
        "code": "Link({\n  url: \"https://example.com\",\n  child: Text(\"Open Website\", {\n    color: Color.blue()\n  })\n})"
      }
    ],
    "!params": [
      {
        "name": "options",
        "type": "?",
        "doc": "",
        "fields": [
          {
            "name": "url",
            "type": "string",
            "doc": "Opened when tapped.",
            "required": true
          },
          {
            "name": "child",
            "type": "+WidgetNode",
            "doc": "The node inside."
          }
        ]
      }
    ]
  },
  "Image": {
    "!doc": "Image widget constructors.",
    "!category": "uiComponents",
    "!subtitle": "Display images from multiple sources",
    "!description": "Builds image nodes from a symbol, a URL or a file, and operates on pixels: download, resize, crop, tint, blur, QR and barcodes. Everything takes and returns the same image value, so results go straight into an Image node or into Canvas.draw.image.\n\nHttp returns text or JSON, so Image.download is the only way to get image bytes into a script. The pixel operations need bytes, and a network image has none until it is fetched.",
    "!examples": [
      {
        "title": "SF Symbol icon",
        "code": "Image.symbol(\"star.fill\", {\n  fontSize: 48,\n  color: Color.yellow()\n})"
      },
      {
        "title": "Network image",
        "code": "Image.network(\"https://example.com/photo.jpg\", {\n  width: Size.infinity,\n  height: Size.fixed(200),\n  cornerRadius: 12,\n  fit: \"cover\"\n})"
      },
      {
        "title": "A downloaded avatar",
        "code": "var photo = Image.download(user.avatarUrl);\nvar avatar = Image.resize(photo, { width: 44 });\n\nRow({ children: [avatar, SizedBox({ width: 8 }), Text(user.name)] })"
      },
      {
        "title": "A QR code",
        "code": "Script.setWidget(new Widget({\n  child: Image.qr(Widget.parameter || \"hello\", { size: 140 }),\n}));"
      }
    ],
    "network": {
      "!type": "fn(url: string, options?: ?) -> +WidgetNode",
      "!doc": "Create an image from a URL.",
      "!params": [
        {
          "name": "url",
          "type": "string",
          "doc": "The image URL. Fetched by the widget before it renders; the pixel operations need Image.download instead.",
          "required": true
        },
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "width",
              "type": "?",
              "doc": "A number of points, or a Size."
            },
            {
              "name": "height",
              "type": "?",
              "doc": "A number of points, or a Size."
            },
            {
              "name": "fit",
              "type": "BoxFit",
              "doc": "A BoxFit, for how the image fills its box."
            },
            {
              "name": "cornerRadius",
              "type": "number",
              "doc": "Rounds the corners."
            },
            {
              "name": "color",
              "type": "+RootlessColor",
              "doc": "Tints an SF Symbol. No effect on a photo."
            },
            {
              "name": "fontSize",
              "type": "number",
              "doc": "Sizes an SF Symbol, which scales with type rather than with a frame."
            },
            {
              "name": "weight",
              "type": "FontWeight",
              "doc": "A FontWeight for an SF Symbol."
            },
            {
              "name": "renderingMode",
              "type": "string",
              "doc": "\"monochrome\", \"hierarchical\", \"palette\" or \"multicolor\", for an SF Symbol."
            },
            {
              "name": "paletteColors",
              "type": "[+RootlessColor]",
              "doc": "Colours for renderingMode \"palette\"."
            }
          ]
        }
      ]
    },
    "symbol": {
      "!type": "fn(name: string, options?: ?) -> +WidgetNode",
      "!doc": "Create an SF Symbol image. weight: 'ultraLight'|'thin'|'light'|'regular'|'medium'|'semibold'|'bold'|'heavy'|'black'. renderingMode: 'monochrome'|'hierarchical'|'palette'|'multicolor'. For palette mode, pass color as an array: [Color.blue(), Color.yellow()].",
      "!params": [
        {
          "name": "name",
          "type": "string",
          "doc": "An SF Symbol name.",
          "required": true
        },
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "width",
              "type": "?",
              "doc": "A number of points, or a Size."
            },
            {
              "name": "height",
              "type": "?",
              "doc": "A number of points, or a Size."
            },
            {
              "name": "fit",
              "type": "BoxFit",
              "doc": "A BoxFit, for how the image fills its box."
            },
            {
              "name": "cornerRadius",
              "type": "number",
              "doc": "Rounds the corners."
            },
            {
              "name": "color",
              "type": "+RootlessColor",
              "doc": "Tints an SF Symbol. No effect on a photo."
            },
            {
              "name": "fontSize",
              "type": "number",
              "doc": "Sizes an SF Symbol, which scales with type rather than with a frame."
            },
            {
              "name": "weight",
              "type": "FontWeight",
              "doc": "A FontWeight for an SF Symbol."
            },
            {
              "name": "renderingMode",
              "type": "string",
              "doc": "\"monochrome\", \"hierarchical\", \"palette\" or \"multicolor\", for an SF Symbol."
            },
            {
              "name": "paletteColors",
              "type": "[+RootlessColor]",
              "doc": "Colours for renderingMode \"palette\"."
            }
          ]
        }
      ]
    },
    "file": {
      "!type": "fn(path: string, options?: ?) -> +WidgetNode",
      "!doc": "Create an image from a file path.",
      "!params": [
        {
          "name": "path",
          "type": "string",
          "doc": "A path on disk.",
          "required": true
        },
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "width",
              "type": "?",
              "doc": "A number of points, or a Size."
            },
            {
              "name": "height",
              "type": "?",
              "doc": "A number of points, or a Size."
            },
            {
              "name": "fit",
              "type": "BoxFit",
              "doc": "A BoxFit, for how the image fills its box."
            },
            {
              "name": "cornerRadius",
              "type": "number",
              "doc": "Rounds the corners."
            },
            {
              "name": "color",
              "type": "+RootlessColor",
              "doc": "Tints an SF Symbol. No effect on a photo."
            },
            {
              "name": "fontSize",
              "type": "number",
              "doc": "Sizes an SF Symbol, which scales with type rather than with a frame."
            },
            {
              "name": "weight",
              "type": "FontWeight",
              "doc": "A FontWeight for an SF Symbol."
            },
            {
              "name": "renderingMode",
              "type": "string",
              "doc": "\"monochrome\", \"hierarchical\", \"palette\" or \"multicolor\", for an SF Symbol."
            },
            {
              "name": "paletteColors",
              "type": "[+RootlessColor]",
              "doc": "Colours for renderingMode \"palette\"."
            }
          ]
        }
      ]
    },
    "download": {
      "!type": "fn(url: string, options?: ?) -> +WidgetNode",
      "!doc": "Fetches an image and returns it with its bytes attached.",
      "!params": [
        {
          "name": "url",
          "type": "string",
          "doc": "The image URL.",
          "required": true
        },
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "timeout",
              "type": "+RootlessDuration",
              "doc": "How long to wait. Default 10 seconds."
            }
          ]
        }
      ]
    },
    "fromBase64": {
      "!type": "fn(encoded: string) -> +WidgetNode",
      "!doc": "An image from base64 data."
    },
    "toBase64": {
      "!type": "fn(image: +WidgetNode) -> string",
      "!doc": "PNG data as base64 - what Http multipart wants for an upload."
    },
    "size": {
      "!type": "fn(image: +WidgetNode) -> ?",
      "!doc": "The image's dimensions.",
      "!returns": "{ width, height } in points."
    },
    "resize": {
      "!type": "fn(image: +WidgetNode, options: ?) -> +WidgetNode",
      "!doc": "Giving one keeps the aspect ratio.",
      "!params": [
        {
          "name": "image",
          "type": "?",
          "doc": "The image to resize.",
          "required": true
        },
        {
          "name": "options",
          "type": "?",
          "doc": "One of width or height keeps the aspect ratio; both stretch.",
          "fields": [
            {
              "name": "width",
              "type": "number",
              "doc": "Target width in pixels."
            },
            {
              "name": "height",
              "type": "number",
              "doc": "Target height in pixels."
            }
          ]
        }
      ]
    },
    "crop": {
      "!type": "fn(image: +WidgetNode, options: ?) -> +WidgetNode",
      "!doc": "Cuts a rectangle out of an image.",
      "!params": [
        {
          "name": "image",
          "type": "?",
          "doc": "The image to crop.",
          "required": true
        },
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "x",
              "type": "number",
              "doc": "Left edge in pixels. Default 0."
            },
            {
              "name": "y",
              "type": "number",
              "doc": "Top edge in pixels. Default 0."
            },
            {
              "name": "width",
              "type": "number",
              "doc": "Width in pixels. Defaults to the rest of the image."
            },
            {
              "name": "height",
              "type": "number",
              "doc": "Height in pixels. Defaults to the rest of the image."
            }
          ]
        }
      ],
      "!returns": "A new image. The original is untouched."
    },
    "tint": {
      "!type": "fn(image: +WidgetNode, color: +RootlessColor) -> +WidgetNode",
      "!doc": "Recolours while keeping transparency - for glyphs and logos."
    },
    "blur": {
      "!type": "fn(image: +WidgetNode, radius?: number) -> +WidgetNode",
      "!doc": "Gaussian blur, cropped back to the original size. Radius defaults to 8."
    },
    "qr": {
      "!type": "fn(text: string, options?: ?) -> +WidgetNode",
      "!doc": "A QR code. where correction is L, M, Q or H.",
      "!params": [
        {
          "name": "text",
          "type": "string",
          "doc": "What the code encodes.",
          "required": true
        },
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "size",
              "type": "number",
              "doc": "Side in pixels. Default 200."
            },
            {
              "name": "correction",
              "type": "string",
              "doc": "Error correction: \"L\", \"M\" (default), \"Q\" or \"H\". Higher survives more damage and holds less data."
            }
          ]
        }
      ]
    },
    "barcode": {
      "!type": "fn(text: string, options?: ?) -> +WidgetNode",
      "!doc": "Type is code128, pdf417 or aztec.",
      "!params": [
        {
          "name": "text",
          "type": "string",
          "doc": "What the code encodes.",
          "required": true
        },
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "type",
              "type": "string",
              "doc": "\"code128\" (default), \"pdf417\" or \"aztec\"."
            },
            {
              "name": "size",
              "type": "number",
              "doc": "Height in pixels. Default 200."
            }
          ]
        }
      ]
    }
  },
  "ProgressBar": {
    "!type": "fn(options?: ?) -> +WidgetNode",
    "!doc": "A progress bar.",
    "!category": "uiComponents",
    "!subtitle": "Linear progress indicator",
    "!description": "A horizontal progress bar with its own track and fill colours.",
    "!examples": [
      {
        "title": "Battery bar",
        "code": "const level = Device.battery.level;\nProgressBar({\n  value: level,\n  progressColor: level > 0.2\n    ? Color.green() : Color.red(),\n  height: 8,\n  cornerRadius: 4\n})"
      }
    ],
    "!params": [
      {
        "name": "options",
        "type": "?",
        "doc": "",
        "fields": [
          {
            "name": "value",
            "type": "number",
            "doc": "0 to 1.",
            "required": true
          },
          {
            "name": "trackColor",
            "type": "+RootlessColor",
            "doc": "The unfilled part."
          },
          {
            "name": "progressColor",
            "type": "+RootlessColor",
            "doc": "The filled part."
          },
          {
            "name": "height",
            "type": "number",
            "doc": "Thickness in points."
          },
          {
            "name": "cornerRadius",
            "type": "number",
            "doc": "Rounds both ends."
          }
        ]
      }
    ]
  },
  "Gauge": {
    "!type": "fn(options?: ?) -> +WidgetNode",
    "!doc": "A circular gauge.",
    "!category": "uiComponents",
    "!subtitle": "Circular progress indicator",
    "!description": "A circular progress ring over a min/max range, with optional content in the middle.",
    "!examples": [
      {
        "title": "Score gauge",
        "code": "Gauge({\n  value: 75, min: 0, max: 100,\n  progressColor: Color.blue(),\n  lineWidth: 8,\n  child: Text(\"75%\", {\n    font: Font.system(16, FontWeight.bold)\n  })\n})"
      }
    ],
    "!params": [
      {
        "name": "options",
        "type": "?",
        "doc": "",
        "fields": [
          {
            "name": "value",
            "type": "number",
            "doc": "Where the ring stops, between min and max.",
            "required": true
          },
          {
            "name": "min",
            "type": "number",
            "doc": "Start of the range. Default 0."
          },
          {
            "name": "max",
            "type": "number",
            "doc": "End of the range. Default 1."
          },
          {
            "name": "trackColor",
            "type": "+RootlessColor",
            "doc": "The unfilled arc."
          },
          {
            "name": "progressColor",
            "type": "+RootlessColor",
            "doc": "The filled arc."
          },
          {
            "name": "lineWidth",
            "type": "number",
            "doc": "Thickness of the ring."
          },
          {
            "name": "size",
            "type": "?",
            "doc": "Diameter, as a number of points or a Size."
          },
          {
            "name": "child",
            "type": "+WidgetNode",
            "doc": "Drawn in the middle."
          }
        ]
      }
    ]
  },
  "Rectangle": {
    "!type": "fn(options?: ?) -> +WidgetNode",
    "!doc": "A rectangle shape.",
    "!category": "uiComponents",
    "!subtitle": "Rounded rectangle shape",
    "!description": "A filled and/or stroked rectangle, with a separate radius per corner.",
    "!examples": [
      {
        "title": "Rounded card background",
        "code": "Rectangle({\n  color: Color.blue(),\n  width: Size.fixed(120), height: Size.fixed(80),\n  topLeadingRadius: 16, topTrailingRadius: 16\n})"
      }
    ],
    "!params": [
      {
        "name": "options",
        "type": "?",
        "doc": "",
        "fields": [
          {
            "name": "color",
            "type": "+RootlessColor",
            "doc": "Fill colour."
          },
          {
            "name": "strokeColor",
            "type": "+RootlessColor",
            "doc": "Outline colour."
          },
          {
            "name": "strokeWidth",
            "type": "number",
            "doc": "Outline thickness."
          },
          {
            "name": "width",
            "type": "?",
            "doc": "A number of points, or a Size."
          },
          {
            "name": "height",
            "type": "?",
            "doc": "A number of points, or a Size."
          },
          {
            "name": "topLeadingRadius",
            "type": "number",
            "doc": "Corner radius."
          },
          {
            "name": "topTrailingRadius",
            "type": "number",
            "doc": "Corner radius."
          },
          {
            "name": "bottomLeadingRadius",
            "type": "number",
            "doc": "Corner radius."
          },
          {
            "name": "bottomTrailingRadius",
            "type": "number",
            "doc": "Corner radius."
          }
        ]
      }
    ]
  },
  "Capsule": {
    "!type": "fn(options?: ?) -> +WidgetNode",
    "!doc": "A capsule shape.",
    "!category": "uiComponents",
    "!subtitle": "Capsule shape",
    "!description": "A filled and/or stroked rectangle with fully rounded ends.",
    "!examples": [
      {
        "title": "Pill badge",
        "code": "Capsule({\n  color: Color.red(),\n  width: Size.fixed(60), height: Size.fixed(24)\n})"
      }
    ],
    "!params": [
      {
        "name": "options",
        "type": "?",
        "doc": "",
        "fields": [
          {
            "name": "color",
            "type": "+RootlessColor",
            "doc": "Fill colour."
          },
          {
            "name": "strokeColor",
            "type": "+RootlessColor",
            "doc": "Outline colour."
          },
          {
            "name": "strokeWidth",
            "type": "number",
            "doc": "Outline thickness."
          },
          {
            "name": "width",
            "type": "?",
            "doc": "A number of points, or a Size."
          },
          {
            "name": "height",
            "type": "?",
            "doc": "A number of points, or a Size."
          }
        ]
      }
    ]
  },
  "Ellipse": {
    "!type": "fn(options?: ?) -> +WidgetNode",
    "!doc": "An ellipse shape.",
    "!category": "uiComponents",
    "!subtitle": "Ellipse shape",
    "!description": "A filled and/or stroked ellipse.",
    "!examples": [
      {
        "title": "Oval shape",
        "code": "Ellipse({\n  color: Color.purple(),\n  strokeColor: Color.white(),\n  strokeWidth: 2,\n  width: Size.fixed(100), height: Size.fixed(60)\n})"
      }
    ],
    "!params": [
      {
        "name": "options",
        "type": "?",
        "doc": "",
        "fields": [
          {
            "name": "color",
            "type": "+RootlessColor",
            "doc": "Fill colour."
          },
          {
            "name": "strokeColor",
            "type": "+RootlessColor",
            "doc": "Outline colour."
          },
          {
            "name": "strokeWidth",
            "type": "number",
            "doc": "Outline thickness."
          },
          {
            "name": "width",
            "type": "?",
            "doc": "A number of points, or a Size."
          },
          {
            "name": "height",
            "type": "?",
            "doc": "A number of points, or a Size."
          }
        ]
      }
    ]
  },
  "Circle": {
    "!type": "fn(options?: ?) -> +WidgetNode",
    "!doc": "A circle shape.",
    "!category": "uiComponents",
    "!subtitle": "Circle shape",
    "!description": "A filled and/or stroked circle.",
    "!examples": [
      {
        "title": "Status indicator",
        "code": "Circle({\n  color: Color.green(),\n  size: Size.fixed(12)\n})"
      }
    ],
    "!params": [
      {
        "name": "options",
        "type": "?",
        "doc": "",
        "fields": [
          {
            "name": "color",
            "type": "+RootlessColor",
            "doc": "Fill colour."
          },
          {
            "name": "strokeColor",
            "type": "+RootlessColor",
            "doc": "Outline colour."
          },
          {
            "name": "strokeWidth",
            "type": "number",
            "doc": "Outline thickness."
          },
          {
            "name": "width",
            "type": "?",
            "doc": "A number of points, or a Size."
          },
          {
            "name": "height",
            "type": "?",
            "doc": "A number of points, or a Size."
          }
        ]
      }
    ]
  },
  "Canvas": {
    "!type": "fn(width: number, height: number, options?: ?) -> +RootlessCanvas",
    "!doc": "A pixel drawing surface. Draw with canvas.draw.*, then call render().",
    "!category": "uiComponents",
    "!subtitle": "Drawing to pixels",
    "!description": "Sides are clamped to 1 to 500 points and at most 1000 draw operations are kept; further calls are dropped. Rectangles use { x, y, w, h }, not width and height. render() gives back an image, which goes into a widget tree or into Image.",
    "!examples": [
      {
        "title": "Simple chart",
        "code": "const c = new Canvas(200, 100, {opaque: false});\nc.draw.rect(\n  {x: 0, y: 60, w: 40, h: 40},\n  {fill: Color.blue()}\n);\nc.draw.rect(\n  {x: 50, y: 30, w: 40, h: 70},\n  {fill: Color.green()}\n);\nc.draw.text(\"Sales\", {x: 0, y: 0, w: 200, h: 20}, {\n  fontSize: 14, color: Color.white()\n});\nconst img = c.render();\nScript.setWidget(img);"
      }
    ],
    "!params": [
      {
        "name": "width",
        "type": "number",
        "doc": "Points, 1 to 500.",
        "required": true
      },
      {
        "name": "height",
        "type": "number",
        "doc": "Points, 1 to 500.",
        "required": true
      },
      {
        "name": "options",
        "type": "?",
        "doc": "",
        "fields": [
          {
            "name": "opaque",
            "type": "bool",
            "doc": "Fill the background before drawing. Default true; false leaves it transparent."
          },
          {
            "name": "scale",
            "type": "bool",
            "doc": "Render at the screen's scale factor. Default true."
          }
        ]
      }
    ]
  },
  "LinearGradient": {
    "!type": "fn(options?: ?) -> +WidgetNode",
    "!doc": "A linear gradient.",
    "!category": "uiComponents",
    "!subtitle": "Linear gradient background",
    "!description": "A gradient along a line, for a Container's backgroundGradient.",
    "!examples": [
      {
        "title": "Gradient container",
        "code": "Container({\n  backgroundGradient: LinearGradient({\n    colors: [Color.blue(), Color.purple()],\n    begin: Alignment.topLeft,\n    end: Alignment.bottomRight\n  }),\n  padding: EdgeInsets.all(16),\n  child: Text(\"Gradient!\", {\n    color: Color.white()\n  })\n})"
      }
    ],
    "!params": [
      {
        "name": "options",
        "type": "?",
        "doc": "",
        "fields": [
          {
            "name": "colors",
            "type": "[+RootlessColor]",
            "doc": "Two or more, in order.",
            "required": true
          },
          {
            "name": "begin",
            "type": "Alignment",
            "doc": "Where it starts. Default top."
          },
          {
            "name": "end",
            "type": "Alignment",
            "doc": "Where it ends. Default bottom."
          }
        ]
      }
    ]
  },
  "MainAxisAlignment": {
    "!doc": "Main axis alignment constants for Column and Row.",
    "!category": "enums",
    "!subtitle": "Main axis distribution options",
    "!description": "Distributes children along the main axis: vertical in a Column, horizontal in a Row. The stack always fills its main axis, so every value has space to work with. start and end push content to one end, center pads both, and the space* values share the gap between children.\n\nspaceAround currently renders the same as spaceEvenly.",
    "start": "string",
    "center": "string",
    "end": "string",
    "spaceBetween": "string",
    "spaceAround": "string",
    "spaceEvenly": "string",
    "!examples": [
      {
        "title": "Pushing content apart",
        "code": "// header at the top, controls at the bottom, gap in between\nColumn({\n  mainAxisAlignment: MainAxisAlignment.spaceBetween,\n  children: [header, controls],\n})"
      }
    ]
  },
  "CrossAxisAlignment": {
    "!doc": "Cross axis alignment constants for Column and Row.",
    "!category": "enums",
    "!subtitle": "Cross axis positioning options",
    "!description": "Positions children across the main axis. \"stretch\" makes them fill it. \"lastBaseline\" lines text up on its baseline and applies to a Row only.",
    "start": "string",
    "center": "string",
    "end": "string",
    "stretch": "string",
    "lastBaseline": "string",
    "!examples": [
      {
        "title": "Baseline-aligned text",
        "code": "// the big number and the small unit sit on one baseline\nRow({\n  crossAxisAlignment: CrossAxisAlignment.lastBaseline,\n  children: [\n    Text(\"24\", { font: Font.system(34, FontWeight.bold) }),\n    Text(\":56\", { font: Font.system(17) }),\n  ],\n})"
      }
    ]
  },
  "FontWeight": {
    "!doc": "Font weight constants.",
    "!category": "enums",
    "!subtitle": "Font weight options",
    "!description": "Weights for Text and for Canvas text.",
    "thin": "string",
    "ultraLight": "string",
    "light": "string",
    "regular": "string",
    "medium": "string",
    "semibold": "string",
    "bold": "string",
    "heavy": "string",
    "black": "string"
  },
  "TextOverflow": {
    "!doc": "Text overflow behavior constants.",
    "!category": "enums",
    "!subtitle": "Text overflow behavior",
    "!description": "What happens to text that does not fit. Used with maxLines on Text.",
    "clip": "string",
    "ellipsis": "string",
    "fade": "string"
  },
  "TextAlign": {
    "!doc": "Text alignment constants.",
    "!category": "enums",
    "!subtitle": "Text alignment options",
    "!description": "Horizontal alignment of text inside its container.",
    "left": "string",
    "center": "string",
    "right": "string",
    "justified": "string"
  },
  "BoxFit": {
    "!doc": "Image fit mode constants.",
    "!category": "enums",
    "!subtitle": "Image content fit modes",
    "!description": "How an image fills its bounds.",
    "cover": "string",
    "contain": "string",
    "fill": "string",
    "none": "string"
  },
  "WidgetFamily": {
    "!doc": "Widget family constants.",
    "!category": "enums",
    "!subtitle": "Widget size families",
    "!description": "The values Widget.currentFamily can take: \"small\", \"medium\", \"large\", \"accessoryRectangular\", \"accessoryInline\", \"accessoryCircular\".",
    "small": "string",
    "medium": "string",
    "large": "string",
    "accessoryRectangular": "string",
    "accessoryInline": "string",
    "accessoryCircular": "string",
    "!examples": [
      {
        "title": "Adapting to size",
        "code": "// Widget.currentFamily is one of these exact strings\nif (Widget.currentFamily === WidgetFamily.large) {\n  Script.setWidget(renderLarge());\n} else {\n  Script.setWidget(renderSmall());\n}"
      }
    ]
  },
  "Weekday": {
    "!doc": "Day of week constants.",
    "!category": "enums",
    "!subtitle": "Day of week values",
    "!description": "The values DateTime.weekday returns.",
    "monday": "string",
    "tuesday": "string",
    "wednesday": "string",
    "thursday": "string",
    "friday": "string",
    "saturday": "string",
    "sunday": "string"
  },
  "BatteryState": {
    "!doc": "Battery state constants.",
    "!category": "enums",
    "!subtitle": "Battery state values",
    "!description": "The values Device.battery.state returns.",
    "charging": "string",
    "full": "string",
    "unplugged": "string",
    "unknown": "string"
  },
  "Appearance": {
    "!doc": "System appearance constants.",
    "!category": "enums",
    "!subtitle": "System appearance mode",
    "!description": "The values Device.screen.appearance returns.",
    "dark": "string",
    "light": "string"
  },
  "ToggleStyle": {
    "!doc": "Toggle appearance constants.",
    "!category": "enums",
    "!subtitle": "Switch or checkbox appearance",
    "!description": "How a Toggle draws itself: a sliding switch, or a checkbox with a struck-through label when on.",
    "!examples": [
      {
        "title": "A checklist item",
        "code": "Toggle({\n  value: done,\n  label: \"Water the plants\",\n  style: ToggleStyle.checkbox,\n  action: \"setDone\",\n})"
      }
    ],
    "switch": "string",
    "checkbox": "string"
  },
  "Format": {
    "!doc": "Locale-aware formatting.",
    "!category": "systemAPIs",
    "!subtitle": "Numbers, units and lists in the reader's region",
    "!description": "Formats numbers, currencies, percentages, byte counts, lists and measurements using the device's locale: grouping separators, decimal marks, unit systems and list conjunctions. A hand-rolled k/m/b shortener or a hardcoded \"km\" is wrong outside the region it was written in.",
    "!examples": [
      {
        "title": "A stats row",
        "code": "Column({ children: [\n  Text(Format.compact(12500)),                       // \"13K\"\n  Text(Format.percent(0.42)),                        // \"42%\"\n  Text(Format.measurement(4213, { unit: \"meters\" })), // \"4.2 km\" or \"2.6 mi\"\n  Text(Format.list([\"Mon\", \"Wed\", \"Fri\"])),          // \"Mon, Wed, and Fri\"\n]})"
      }
    ],
    "number": {
      "!type": "fn(value: number, options?: ?) -> string",
      "!doc": "Decimal number.",
      "!params": [
        {
          "name": "value",
          "type": "number",
          "doc": "The number to format.",
          "required": true
        },
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "digits",
              "type": "number",
              "doc": "Maximum decimal places. Default 0."
            },
            {
              "name": "minDigits",
              "type": "number",
              "doc": "Minimum decimal places, padded with zeros. Default 0."
            },
            {
              "name": "grouping",
              "type": "bool",
              "doc": "Thousands separator. Default true."
            }
          ]
        }
      ]
    },
    "compact": {
      "!type": "fn(value: number, options?: ?) -> string",
      "!doc": "Shortened number, 12500 becomes \"13K\", localised.",
      "!params": [
        {
          "name": "value",
          "type": "number",
          "doc": "The number to shorten.",
          "required": true
        },
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "digits",
              "type": "number",
              "doc": "Maximum decimal places. Default 1."
            }
          ]
        }
      ]
    },
    "percent": {
      "!type": "fn(value: number, options?: ?) -> string",
      "!doc": "Percentage from a fraction: 0.42 becomes \"42%\".",
      "!params": [
        {
          "name": "value",
          "type": "number",
          "doc": "A fraction, not 0 to 100. 0.42 gives \"42%\".",
          "required": true
        },
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "digits",
              "type": "number",
              "doc": "Maximum decimal places. Default 0."
            }
          ]
        }
      ]
    },
    "currency": {
      "!type": "fn(value: number, options?: ?) -> string",
      "!doc": "Money. Defaults to the region's currency.",
      "!params": [
        {
          "name": "value",
          "type": "number",
          "doc": "The amount.",
          "required": true
        },
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "code",
              "type": "string",
              "doc": "ISO 4217 code such as \"EUR\". Defaults to the device's currency."
            },
            {
              "name": "digits",
              "type": "number",
              "doc": "Fixed decimal places. Defaults to what the currency uses."
            }
          ]
        }
      ]
    },
    "bytes": {
      "!type": "fn(value: number, options?: ?) -> string",
      "!doc": "Byte count, 1048576 becomes \"1 MB\". (\"file\" or \"memory\").",
      "!params": [
        {
          "name": "value",
          "type": "number",
          "doc": "A count of bytes.",
          "required": true
        },
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "style",
              "type": "string",
              "doc": "\"memory\" counts in powers of 1024. Anything else counts in powers of 1000, which is what a file size uses."
            }
          ]
        }
      ]
    },
    "ordinal": {
      "!type": "fn(value: number) -> string",
      "!doc": "Ordinal: 3 becomes \"3rd\"."
    },
    "list": {
      "!type": "fn(items: [string]) -> string",
      "!doc": "Joins with the region's conjunction: \"a, b, and c\".",
      "!params": [
        {
          "name": "items",
          "type": "[string]",
          "doc": "The strings to join.",
          "required": true
        }
      ],
      "!returns": "The list joined the way the device's language does it, conjunction included."
    },
    "measurement": {
      "!type": "fn(value: number, options: ?) -> string",
      "!doc": "A value with a unit, converted to whatever the region uses. `unit` is required. One of millimeters, centimeters, meters, kilometers, inches, feet, yards, miles, celsius, fahrenheit, kelvin, metersPerSecond, kilometersPerHour, milesPerHour, knots, grams, kilograms, ounces, pounds, kilocalories, kilojoules. Set `to` to pin the output unit instead of letting the locale choose.",
      "!params": [
        {
          "name": "value",
          "type": "number",
          "doc": "The amount, in the unit named below.",
          "required": true
        },
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "unit",
              "type": "string",
              "doc": "The unit `value` is in.",
              "required": true
            },
            {
              "name": "to",
              "type": "string",
              "doc": "Convert to this unit. Left out, the device's region decides."
            },
            {
              "name": "style",
              "type": "string",
              "doc": "\"short\", \"medium\" or \"long\", for \"km\", \"km\" or \"kilometers\"."
            },
            {
              "name": "digits",
              "type": "number",
              "doc": "Maximum decimal places."
            }
          ]
        }
      ]
    },
    "usesMetric": {
      "!type": "bool",
      "!doc": "True where this device's region uses the metric system."
    },
    "locale": {
      "!type": "string",
      "!doc": "The device's locale identifier, e.g. \"en_US\"."
    }
  },
  "Distance": {
    "!doc": "A length, with unit accessors.",
    "!category": "primitives",
    "!subtitle": "Metres, kilometres, miles. One value",
    "!description": "A length, shaped like Duration: build one in any unit and read it back in another. formatted() goes through Format, so it prints the unit the reader's region uses.",
    "!examples": [
      {
        "title": "Showing a distance",
        "code": "var d = Location.current().distanceTo(home);\nText(d.formatted());        // \"4.2 km\" or \"2.6 mi\"\nText(String(Math.round(d.inMeters)) + \" m\");"
      }
    ],
    "meters": {
      "!type": "fn(value: number) -> +RootlessDistance",
      "!doc": "A distance in metres."
    },
    "kilometers": {
      "!type": "fn(value: number) -> +RootlessDistance",
      "!doc": "A distance in kilometres."
    },
    "miles": {
      "!type": "fn(value: number) -> +RootlessDistance",
      "!doc": "A distance in miles."
    },
    "feet": {
      "!type": "fn(value: number) -> +RootlessDistance",
      "!doc": "A distance in feet."
    }
  },
  "Motion": {
    "!doc": "The device's motion sensors, read one sample at a time.",
    "!category": "systemAPIs",
    "!subtitle": "Sensors, steps and activity",
    "!description": "Everything CoreMotion reports. Each sensor is read once per call: the reading starts, the first sample comes back, the sensor stops. A script cannot subscribe to a stream, because a script runs to its last line and is then serialised, with no event loop left to receive a second sample. Ask for a series by sampling in a loop.\n\nUnits are CoreMotion's own: g for acceleration, radians per second for rotation, microtesla for the magnetic field, radians for attitude, kilopascals for pressure.\n\nNothing here answers on the simulator, and history reaches back seven days. Check Motion.isAvailable before reading a sensor that the device may not have.",
    "!examples": [
      {
        "title": "How level the device is",
        "code": "var a = Motion.attitude();\nvar degrees = a.roll * 180 / Math.PI;"
      }
    ],
    "isAvailable": {
      "!type": "?",
      "!doc": "Which sensors this device has: { accelerometer, gyroscope, magnetometer, attitude, barometer, steps, activity }.",
      "!doctype": "object"
    },
    "accelerometer": {
      "!type": "fn() -> ?",
      "!doc": "Acceleration on the three axes, in g, gravity included.",
      "!returns": "{ x, y, z } in g."
    },
    "gyroscope": {
      "!type": "fn() -> ?",
      "!doc": "Rotation rate on the three axes, in radians per second.",
      "!returns": "{ x, y, z } in rad/s."
    },
    "magnetometer": {
      "!type": "fn() -> ?",
      "!doc": "The magnetic field on the three axes, in microtesla. Raw, so it includes whatever the device itself is generating.",
      "!returns": "{ x, y, z } in µT."
    },
    "attitude": {
      "!type": "fn() -> ?",
      "!doc": "The fused reading: where the device is pointing, with gravity told apart from the acceleration you caused.",
      "!returns": "{ pitch, roll, yaw } in radians, plus gravity and userAcceleration as { x, y, z } in g, rotationRate in rad/s, and heading in degrees from magnetic north when the compass has settled. heading is absent while it has not."
    },
    "pressure": {
      "!type": "fn() -> ?",
      "!doc": "The barometer.",
      "!returns": "{ kPa, relativeAltitude }. relativeAltitude is metres climbed since the reading began, so on a single sample it is near zero."
    },
    "steps": {
      "!type": "fn(from?: ?, to?: ?) -> ?",
      "!doc": "Steps over a window. With no arguments, since midnight; Motion.steps(DateTime.today()) is the same call written out. Anything older than seven days is clamped to seven days ago, which is all CoreMotion keeps.",
      "!returns": "{ steps, distance, floorsAscended, floorsDescended, averageActivePace, from, to }. distance is a Distance, averageActivePace is seconds per metre and is absent when the window holds no movement, from and to are epoch seconds."
    },
    "activity": {
      "!type": "fn(from?: ?, to?: ?) -> ?",
      "!doc": "What the device was doing over a window, as a list of spans. With no arguments, since midnight.",
      "!returns": "[{ kind, confidence, start, end }]. kind is stationary, walking, running, cycling or automotive; confidence is low, medium or high; start and end are epoch seconds. CoreMotion records a change rather than a span, so each entry runs until the next one begins and the last runs to the end of the window."
    },
    "activityTotals": {
      "!type": "fn(from?: ?, to?: ?) -> ?",
      "!doc": "How long was spent doing what over a window. With no arguments, since midnight.",
      "!returns": "{ stationary, walking, running, cycling, automotive }, each a Duration. A kind the window holds none of is absent."
    }
  },
  "Crypto": {
    "!doc": "Hashing, HMAC and base64.",
    "!category": "systemAPIs",
    "!subtitle": "Digests and signing",
    "!description": "Hashing, HMAC, base64 and random bytes. JavaScriptCore is an ECMAScript engine, not a browser, so it has no atob, btoa or crypto object. Needed for signed APIs (AWS, Cloudflare, anything HMAC) and for Basic auth.",
    "!examples": [
      {
        "title": "A signed request",
        "code": "var stamp = String(Math.floor(Date.now() / 1000));\nvar signature = Crypto.hmacSHA256(stamp + path, Keychain.get(\"apiSecret\"));\n\nHttp.get(url, { headers: {\n  \"X-Timestamp\": stamp,\n  \"X-Signature\": signature,\n}});"
      }
    ],
    "md5": {
      "!type": "fn(text: string) -> string",
      "!doc": "MD5 digest, hex."
    },
    "sha1": {
      "!type": "fn(text: string) -> string",
      "!doc": "SHA-1 digest, hex."
    },
    "sha256": {
      "!type": "fn(text: string) -> string",
      "!doc": "SHA-256 digest, hex."
    },
    "sha384": {
      "!type": "fn(text: string) -> string",
      "!doc": "SHA-384 digest, hex."
    },
    "sha512": {
      "!type": "fn(text: string) -> string",
      "!doc": "SHA-512 digest, hex."
    },
    "hmacSHA256": {
      "!type": "fn(message: string, key: string) -> string",
      "!doc": "HMAC-SHA256, hex."
    },
    "hmacSHA512": {
      "!type": "fn(message: string, key: string) -> string",
      "!doc": "HMAC-SHA512, hex."
    },
    "hmacSHA256Base64": {
      "!type": "fn(message: string, key: string) -> string",
      "!doc": "HMAC-SHA256, base64. What most signing schemes want."
    },
    "base64Encode": {
      "!type": "fn(text: string) -> string",
      "!doc": "UTF-8 text to base64."
    },
    "base64Decode": {
      "!type": "fn(encoded: string) -> string",
      "!doc": "Base64 back to text, or null if it isn't valid."
    },
    "randomUUID": {
      "!type": "fn() -> string",
      "!doc": "A new UUID string."
    },
    "randomBytes": {
      "!type": "fn(count: number) -> string",
      "!doc": "Cryptographically random bytes as hex, up to 1024."
    }
  },
  "Keychain": {
    "!doc": "Secrets, kept out of script source.",
    "!category": "systemAPIs",
    "!subtitle": "Encrypted per-script storage",
    "!description": "Encrypted storage for API keys, scoped per script and readable from the widget. Put keys here rather than in a string literal: script source travels inside any exported .js file, so a key written into a script is shared with it.",
    "!examples": [
      {
        "title": "Using a stored key",
        "code": "// once, from a script you run in the app:\n// Keychain.set(\"apiKey\", \"…\");\n\nvar key = Keychain.get(\"apiKey\");\nif (!key) {\n  Script.setWidget(new Widget({ child: Text(\"No API key set\") }));\n} else {\n  var res = Http.get(url + \"?appid=\" + key);\n}"
      }
    ],
    "set": {
      "!type": "fn(key: string, value: string) -> bool",
      "!doc": "Stores a value. Returns false if it could not be written."
    },
    "get": {
      "!type": "fn(key: string) -> string",
      "!doc": "The stored value, or null."
    },
    "has": {
      "!type": "fn(key: string) -> bool",
      "!doc": "Whether a value is stored under this key."
    },
    "delete": {
      "!type": "fn(key: string) -> bool",
      "!doc": "Removes the value."
    }
  },
  "Pasteboard": {
    "!doc": "The system clipboard.",
    "!category": "systemAPIs",
    "!subtitle": "Copy and paste text",
    "!description": "The clipboard. In-app only: a widget has no access and the call fails at once rather than spending its time budget.",
    "!examples": [
      {
        "title": "Copying a result",
        "code": "if (Pasteboard.isAvailable) {\n  Pasteboard.copy(result);\n  Haptics.play(\"success\");\n}"
      }
    ],
    "isAvailable": {
      "!type": "bool",
      "!doc": "False in a widget."
    },
    "copy": {
      "!type": "fn(text: string)",
      "!doc": "Puts text on the clipboard."
    },
    "paste": {
      "!type": "fn() -> string",
      "!doc": "Text from the clipboard, or null."
    }
  },
  "Haptics": {
    "!doc": "Haptic feedback.",
    "!category": "systemAPIs",
    "!subtitle": "Taps and buzzes",
    "!description": "In-app only, like Pasteboard. Kinds: \"success\", \"warning\", \"error\", \"light\", \"medium\", \"heavy\", \"selection\".",
    "isAvailable": {
      "!type": "bool",
      "!doc": "False in a widget."
    },
    "play": {
      "!type": "fn(kind?: string)",
      "!doc": "Plays feedback. Defaults to \"medium\"."
    }
  },
  "Font": {
    "!doc": "Type as one value.",
    "!category": "primitives",
    "!subtitle": "Semantic styles and exact sizes",
    "!description": "One value carrying size, weight, family and design. The named styles follow the reader's text-size setting; Font.system pins an exact number and does not.",
    "!examples": [
      {
        "title": "Setting type",
        "code": "Text(\"Steps\", { font: Font.HEADLINE })\nText(\"4,213\", { font: Font.system(26, FontWeight.bold) })\nText(\"today\", { font: Font.CAPTION.withWeight(FontWeight.medium) })\nText(\"Hello\", { font: Font.custom(\"Avenir\", 20) })"
      }
    ],
    "LARGE_TITLE": "+RootlessFont",
    "TITLE": "+RootlessFont",
    "TITLE2": "+RootlessFont",
    "TITLE3": "+RootlessFont",
    "HEADLINE": "+RootlessFont",
    "SUBHEADLINE": "+RootlessFont",
    "BODY": "+RootlessFont",
    "CALLOUT": "+RootlessFont",
    "FOOTNOTE": "+RootlessFont",
    "CAPTION": "+RootlessFont",
    "CAPTION2": "+RootlessFont",
    "system": {
      "!type": "fn(size: number, weight?: string, design?: string) -> +RootlessFont",
      "!doc": "A system font at an exact point size, optionally in a design.",
      "!params": [
        {
          "name": "size",
          "type": "number",
          "doc": "Point size.",
          "required": true
        },
        {
          "name": "weight",
          "type": "FontWeight",
          "doc": "Defaults to regular."
        },
        {
          "name": "design",
          "type": "FontDesign",
          "doc": "Defaults to the system font."
        }
      ]
    },
    "custom": {
      "!type": "fn(name: string, size: number, weight?: string) -> +RootlessFont",
      "!doc": "A named family such as \"Avenir\". Device.fonts lists what is installed; an unknown name falls back to the system font.",
      "!params": [
        {
          "name": "name",
          "type": "string",
          "doc": "A family name from Device.fonts.",
          "required": true
        },
        {
          "name": "size",
          "type": "number",
          "doc": "Point size.",
          "required": true
        },
        {
          "name": "weight",
          "type": "FontWeight",
          "doc": "Defaults to regular."
        }
      ]
    }
  },
  "FontDesign": {
    "!doc": "Cuts of the system font.",
    "!category": "enums",
    "!subtitle": "Rounded, serif, monospaced",
    "!description": "A variant of the system font, not a family. \"SF Pro Rounded\" cannot be reached with Font.custom and does not appear in Device.fonts: ask for the design instead. Has no effect on a font that names a family.",
    "!examples": [
      {
        "title": "Rounded and monospaced",
        "code": "Text(\"4,213\", { font: Font.system(26, FontWeight.bold).rounded() })\nText(\"12:04\", { font: Font.TITLE.monospaced() })   // digits stop jittering\nText(\"Body\", { font: Font.BODY.withDesign(FontDesign.serif) })"
      }
    ],
    "default": "string",
    "rounded": "string",
    "serif": "string",
    "monospaced": "string"
  },
  "Alert": {
    "!doc": "Modal alerts and action sheets.",
    "!category": "systemAPIs",
    "!subtitle": "Ask a question, get an answer",
    "!description": "Blocks until the person taps something, then returns the index of the action they chose, or -1 if dismissed. In-app only: a widget cannot present anything and the call fails at once. Check isAvailable.",
    "!examples": [
      {
        "title": "Confirming",
        "code": "var choice = Alert.present(\"Delete?\", \"This cannot be undone.\", [\"Delete\", \"Cancel\"]);\nif (choice === 0) { Storage.delete(\"draft\"); }"
      }
    ],
    "isAvailable": {
      "!type": "bool",
      "!doc": "False in a widget."
    },
    "present": {
      "!type": "fn(title: string, message?: string, actions?: [string]) -> number",
      "!doc": "A modal alert. Actions default to [\"OK\"]. Returns the tapped index."
    },
    "sheet": {
      "!type": "fn(title: string, message?: string, actions?: [string]) -> number",
      "!doc": "The same as an action sheet. The last action is treated as the cancel one."
    }
  },
  "Safari": {
    "!doc": "The web browser.",
    "!category": "systemAPIs",
    "!subtitle": "In-app browser, or hand off to Safari",
    "!description": "Safari.present opens a page inside the app and returns when it closes. Safari.open hands the URL to the system browser and carries on. In-app only; check isAvailable.",
    "!examples": [
      {
        "title": "Reading a link",
        "code": "Safari.present(\"https://example.com\", { readerMode: true });"
      }
    ],
    "isAvailable": {
      "!type": "bool",
      "!doc": "False in a widget."
    },
    "present": {
      "!type": "fn(url: string, options?: ?) -> void",
      "!doc": "In-app browser. Blocks until it is dismissed.",
      "!params": [
        {
          "name": "url",
          "type": "string",
          "doc": "The page to open.",
          "required": true
        },
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "readerMode",
              "type": "bool",
              "doc": "Open in Reader when the page supports it."
            }
          ]
        }
      ]
    },
    "open": {
      "!type": "fn(url: string) -> void",
      "!doc": "Hands the URL to the system browser."
    }
  },
  "ShareSheet": {
    "!doc": "The system share sheet.",
    "!category": "systemAPIs",
    "!subtitle": "Share text, links and files",
    "!description": "Takes a string or an array of them; anything that parses as a URL is shared as a link. Returns true if something was chosen. In-app only; check isAvailable.",
    "!examples": [
      {
        "title": "Sharing a result",
        "code": "ShareSheet.present([\"Today: 4,213 steps\", \"https://example.com\"]);"
      }
    ],
    "isAvailable": {
      "!type": "bool",
      "!doc": "False in a widget."
    },
    "present": {
      "!type": "fn(items: ?) -> bool",
      "!doc": "Shares a string or an array of strings.",
      "!params": [
        {
          "name": "items",
          "type": "?",
          "doc": "A string, or an array of strings. A string that parses as a URL is shared as a link, so the sheet offers Open in Safari rather than treating it as prose.",
          "required": true
        }
      ],
      "!returns": "true when the user completed a share, false when they dismissed the sheet."
    }
  },
  "QuickLook": {
    "!doc": "Preview a file.",
    "!category": "systemAPIs",
    "!subtitle": "Documents, images and text",
    "!description": "Previews a file. Text is written to a temporary file first. In-app only; check isAvailable.",
    "!examples": [
      {
        "title": "Previewing output",
        "code": "QuickLook.text(JSON.stringify(data, null, 2), \"response\");"
      }
    ],
    "isAvailable": {
      "!type": "bool",
      "!doc": "False in a widget."
    },
    "present": {
      "!type": "fn(path: string) -> void",
      "!doc": "Previews the file at a path."
    },
    "text": {
      "!type": "fn(text: string, name?: string) -> void",
      "!doc": "Previews a string, written to a temporary .txt file."
    }
  },
  "XML": {
    "!doc": "XML documents, and RSS or Atom feeds.",
    "!category": "systemAPIs",
    "!subtitle": "Parse XML and news feeds",
    "!description": "XML.parse returns a tree of plain objects. XML.feed flattens RSS and Atom to one shape, so a script does not need to know which it fetched. Most APIs return JSON and res.json covers those; feeds do not.",
    "!examples": [
      {
        "title": "Latest headlines",
        "code": "var res = Http.get(\"https://example.com/feed.xml\", { cache: Duration.minutes(30) });\nvar feed = XML.feed(res.text);\n\nColumn({ children: feed.items.slice(0, 3).map(function (item) {\n  return Text(item.title, { font: Font.FOOTNOTE, maxLines: 2 });\n}) })"
      }
    ],
    "parse": {
      "!type": "fn(text: string) -> ?",
      "!doc": "Parses XML into plain objects.",
      "!returns": "A tree of { name, attributes, text, children }."
    },
    "feed": {
      "!type": "fn(text: string) -> ?",
      "!doc": "Parses RSS or Atom into one shape, whichever it was.",
      "!returns": "{ title, link, description, items }, where each item is { title, link, summary, author, published, id }."
    }
  },
  "Mail": {
    "!doc": "Compose an email.",
    "!category": "systemAPIs",
    "!subtitle": "The mail composer",
    "!description": "Opens the system composer prefilled and blocks until it closes, returning \"sent\", \"saved\", \"cancelled\" or \"failed\". In-app only, and isAvailable is also false on a device with no account set up.",
    "!examples": [
      {
        "title": "Sending a report",
        "code": "if (Mail.isAvailable) {\n  Mail.compose({ to: \"me@example.com\", subject: \"Steps\", body: summary });\n}"
      }
    ],
    "isAvailable": {
      "!type": "bool",
      "!doc": "False in a widget, or with no mail account set up."
    },
    "compose": {
      "!type": "fn(options: ?) -> string",
      "!doc": "Recipients take a string or an array.",
      "!params": [
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "to",
              "type": "[string]",
              "doc": "Recipients."
            },
            {
              "name": "cc",
              "type": "[string]",
              "doc": "Carbon copy."
            },
            {
              "name": "bcc",
              "type": "[string]",
              "doc": "Blind carbon copy."
            },
            {
              "name": "subject",
              "type": "string",
              "doc": "The subject line."
            },
            {
              "name": "body",
              "type": "string",
              "doc": "The message."
            },
            {
              "name": "isHTML",
              "type": "bool",
              "doc": "Treat body as HTML."
            }
          ]
        }
      ]
    }
  },
  "Message": {
    "!doc": "Compose a message.",
    "!category": "systemAPIs",
    "!subtitle": "The Messages composer",
    "!description": "Opens the system composer prefilled and blocks until it closes, returning \"sent\", \"cancelled\" or \"failed\". In-app only, and isAvailable is also false on a device that cannot send messages.",
    "!examples": [
      {
        "title": "Texting a result",
        "code": "if (Message.isAvailable) {\n  Message.compose({ to: \"+15551234\", body: \"On my way\" });\n}"
      }
    ],
    "isAvailable": {
      "!type": "bool",
      "!doc": "False in a widget, or where the device can't send texts."
    },
    "compose": {
      "!type": "fn(options: ?) -> string",
      "!doc": "Recipients take a string or an array.",
      "!params": [
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "to",
              "type": "[string]",
              "doc": "Recipients."
            },
            {
              "name": "body",
              "type": "string",
              "doc": "The message."
            }
          ]
        }
      ]
    }
  },
  "Reminder": {
    "!type": "fn(config?: ?) -> +RootlessReminder",
    "!doc": "Creates a reminder. Call save() on it to write it.",
    "!category": "systemAPIs",
    "!subtitle": "Read and write reminders",
    "!description": "A reminder, shaped like Event: a constructor, a fetch, and save()/remove() on what you get back. Due dates are DateTimes; EventKit stores date components and this converts both ways. Priority is 0 for none, 1 to 4 high, 5 medium, 6 to 9 low, matching EventKit. Needs reminders access.",
    "!examples": [
      {
        "title": "Today's list",
        "code": "var todo = Reminder.fetch({ completed: false, list: \"Personal\" });\n\nColumn({ children: todo.slice(0, 4).map(function (r) {\n  return Toggle({\n    value: r.isCompleted,\n    label: r.title,\n    style: ToggleStyle.checkbox,\n    action: \"done\",\n    payload: { id: r.identifier },\n  });\n}) })"
      },
      {
        "title": "Adding one",
        "code": "var r = new Reminder({\n  title: \"Water the plants\",\n  dueDate: DateTime.now().add(Duration.hours(3)),\n  priority: 5,\n  list: \"Personal\",\n});\nr.save();"
      }
    ],
    "fetch": {
      "!type": "fn(query?: ?) -> [+RootlessReminder]",
      "!doc": "Reminders matching a query.",
      "!params": [
        {
          "name": "query",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "completed",
              "type": "bool",
              "doc": "Only done, or only not. Left out, both."
            },
            {
              "name": "list",
              "type": "string",
              "doc": "A list title or identifier."
            },
            {
              "name": "from",
              "type": "+RootlessDateTime",
              "doc": "Due no earlier than this."
            },
            {
              "name": "to",
              "type": "+RootlessDateTime",
              "doc": "Due no later than this."
            }
          ]
        }
      ]
    },
    "lists": {
      "!type": "fn() -> [?]",
      "!doc": "Every reminder list.",
      "!returns": "One { identifier, title, color, isEditable } per list."
    },
    "!params": [
      {
        "name": "config",
        "type": "?",
        "doc": "",
        "fields": [
          {
            "name": "title",
            "type": "string",
            "doc": "What it is called.",
            "required": true
          },
          {
            "name": "notes",
            "type": "string",
            "doc": "Free text."
          },
          {
            "name": "dueDate",
            "type": "+RootlessDateTime",
            "doc": "When it is due."
          },
          {
            "name": "priority",
            "type": "number",
            "doc": "0 none, 1 to 4 high, 5 medium, 6 to 9 low."
          },
          {
            "name": "list",
            "type": "string",
            "doc": "Which list, by title or identifier. The default one when left out."
          },
          {
            "name": "isCompleted",
            "type": "bool",
            "doc": "Create it already done."
          },
          {
            "name": "url",
            "type": "string",
            "doc": "A link to attach."
          }
        ]
      }
    ]
  },
  "WebView": {
    "!type": "fn() -> +RootlessWebView",
    "!doc": "Create a web view.",
    "!category": "systemAPIs",
    "!subtitle": "Load a page and read from it",
    "!description": "Loads a page and runs JavaScript inside it, which reaches anything a browser can see. Use WebView.read to load a page and pull one value out; the constructor is for asking several questions of one page. It can also show the page.\n\nEvery call blocks until the page or the script finishes. In-app only: a widget has no view hierarchy to build one in, so check WebView.isAvailable. A web view is heavy; call close() when you are done rather than leaving several open.",
    "!examples": [
      {
        "title": "Scraping one value",
        "code": "// A site with no API at all\nvar price = WebView.read(\n  \"https://example.com/product\",\n  \"document.querySelector('.price').innerText\"\n);\n\nScript.setWidget(new Widget({ child: Text(price, { font: Font.TITLE }) }));"
      },
      {
        "title": "Several questions, one page",
        "code": "var web = new WebView();\nweb.loadURL(\"https://example.com\");\n\nvar title = web.evaluate(\"document.title\");\nvar links = web.evaluate(\n  \"Array.from(document.querySelectorAll('a')).slice(0,5).map(a => a.href)\"\n);\nweb.close();"
      }
    ],
    "isAvailable": {
      "!type": "bool",
      "!doc": "False in a widget."
    },
    "read": {
      "!type": "fn(url: string, script: string, options?: ?) -> ?",
      "!doc": "Loads a page, evaluates the script in it, returns the result, and throws the view away.",
      "!params": [
        {
          "name": "url",
          "type": "string",
          "doc": "The page to load.",
          "required": true
        },
        {
          "name": "script",
          "type": "string",
          "doc": "JavaScript evaluated in the page. Its last expression is the result.",
          "required": true
        },
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "timeout",
              "type": "+RootlessDuration",
              "doc": "How long to wait for the load and the script. Default 30 seconds."
            },
            {
              "name": "baseURL",
              "type": "string",
              "doc": "Resolves relative URLs when the page is passed as HTML rather than fetched."
            }
          ]
        }
      ]
    }
  },
  "require": {
    "!type": "fn(name: string) -> ?",
    "!doc": "Loads another script from your library and returns what it exported. The module sets module.exports; names are matched case-insensitively against script titles. Requiring the same module twice gives the same object, so state is shared. Cycles and chains deeper than 8 are refused with a message naming the path.",
    "!category": "systemAPIs",
    "!subtitle": "One script using another",
    "!description": "Loads another script from your library and returns its exports. A module is an ordinary script, so it stays editable, runnable and previewable. It runs in the same context and sees the same globals and bridges. A module that throws while loading stops the run, and the error names the module and the line inside it.",
    "!examples": [
      {
        "title": "A shared helper",
        "code": "// In a script titled \"Helpers\"\nfunction greet(name) {\n  return \"Hello, \" + name;\n}\n\nmodule.exports = { greet: greet };\n\n// In any other script\nvar helpers = require(\"Helpers\");\n\nScript.setWidget(new Widget({\n  child: Text(helpers.greet(\"world\")),\n}));"
      }
    ],
    "!params": [
      {
        "name": "name",
        "type": "string",
        "doc": "The other script's title, matched without regard to case or surrounding spaces.",
        "required": true
      }
    ],
    "!returns": "Whatever the module assigned to module.exports."
  },
  "Path": {
    "!type": "fn(options: ?) -> +WidgetNode",
    "!doc": "A vector outline, drawn by SwiftUI rather than rasterised.",
    "!category": "uiComponents",
    "!subtitle": "Custom shapes, drawn as vectors",
    "!description": "A vector outline, drawn by SwiftUI rather than rasterised: it scales with its frame, costs no memory and takes part in layout. viewBox sets your own coordinate space and the outline is fitted to whatever size the widget gives it, so one component serves every family. fit defaults to BoxFit.contain, which keeps the proportions; BoxFit.fill stretches each axis independently. One fill and one stroke per shape: a multi-coloured icon is several Paths in a Stack sharing a viewBox.",
    "!examples": [
      {
        "title": "Sparkline",
        "code": "var values = [3, 7, 4, 9, 6, 11, 8];\nvar points = values.map(function (v, i) {\n  return [i * (100 / (values.length - 1)), 12 - v];\n});\n\nScript.setWidget(new Widget({\n  padding: EdgeInsets.all(16),\n  child: Path({\n    viewBox: { width: 100, height: 12 },\n    fit: BoxFit.fill,\n    commands: Path.polyline(points),\n    stroke: Color.blue(),\n    strokeWidth: 2,\n    strokeCap: \"round\",\n  }),\n}));"
      },
      {
        "title": "An icon pasted from a drawing tool",
        "code": "// Copy the `d` attribute out of Figma, Illustrator or an .svg file.\nPath({\n  viewBox: { width: 24, height: 24 },\n  commands: Path.d(\"M12 2 L22 20 L2 20 Z\"),\n  fill: Color.orange(),\n})"
      }
    ],
    "move": {
      "!type": "fn(x: number, y: number) -> ?",
      "!doc": "Starts a new subpath at a point, drawing nothing on the way there.",
      "!params": [
        {
          "name": "x",
          "type": "number",
          "doc": "Horizontal position, in viewBox units.",
          "required": true
        },
        {
          "name": "y",
          "type": "number",
          "doc": "Vertical position, in viewBox units.",
          "required": true
        }
      ],
      "!returns": "One command."
    },
    "line": {
      "!type": "fn(x: number, y: number) -> ?",
      "!doc": "A straight segment from the current point.",
      "!params": [
        {
          "name": "x",
          "type": "number",
          "doc": "Horizontal position, in viewBox units.",
          "required": true
        },
        {
          "name": "y",
          "type": "number",
          "doc": "Vertical position, in viewBox units.",
          "required": true
        }
      ],
      "!returns": "One command."
    },
    "curve": {
      "!type": "fn(x: number, y: number, c1x: number, c1y: number, c2x: number, c2y: number) -> ?",
      "!doc": "A cubic Bezier from the current point. The end point comes first, then the two control points.",
      "!params": [
        {
          "name": "x",
          "type": "number",
          "doc": "End point, horizontal.",
          "required": true
        },
        {
          "name": "y",
          "type": "number",
          "doc": "End point, vertical.",
          "required": true
        },
        {
          "name": "c1x",
          "type": "number",
          "doc": "First control point, horizontal. Pulls the curve away from the start.",
          "required": true
        },
        {
          "name": "c1y",
          "type": "number",
          "doc": "First control point, vertical.",
          "required": true
        },
        {
          "name": "c2x",
          "type": "number",
          "doc": "Second control point, horizontal. Pulls it into the end.",
          "required": true
        },
        {
          "name": "c2y",
          "type": "number",
          "doc": "Second control point, vertical.",
          "required": true
        }
      ],
      "!returns": "One command."
    },
    "quad": {
      "!type": "fn(x: number, y: number, cx: number, cy: number) -> ?",
      "!doc": "A quadratic Bezier from the current point. The end point comes first, then the single control point.",
      "!params": [
        {
          "name": "x",
          "type": "number",
          "doc": "End point, horizontal.",
          "required": true
        },
        {
          "name": "y",
          "type": "number",
          "doc": "End point, vertical.",
          "required": true
        },
        {
          "name": "cx",
          "type": "number",
          "doc": "Control point, horizontal.",
          "required": true
        },
        {
          "name": "cy",
          "type": "number",
          "doc": "Control point, vertical.",
          "required": true
        }
      ],
      "!returns": "One command."
    },
    "arc": {
      "!type": "fn(cx: number, cy: number, radius: number, startDegrees: number, endDegrees: number, counterClockwise?: bool) -> ?",
      "!doc": "A circular arc. For an elliptical one, write it as SVG and pass it to Path.d.",
      "!params": [
        {
          "name": "cx",
          "type": "number",
          "doc": "Centre, horizontal.",
          "required": true
        },
        {
          "name": "cy",
          "type": "number",
          "doc": "Centre, vertical.",
          "required": true
        },
        {
          "name": "radius",
          "type": "number",
          "doc": "In viewBox units.",
          "required": true
        },
        {
          "name": "startDegrees",
          "type": "number",
          "doc": "0 is three o'clock and the angle grows clockwise.",
          "required": true
        },
        {
          "name": "endDegrees",
          "type": "number",
          "doc": "Where it stops. A full circle is 0 to 360.",
          "required": true
        },
        {
          "name": "counterClockwise",
          "type": "bool",
          "doc": "Sweep the other way round. Default false."
        }
      ],
      "!returns": "One command."
    },
    "close": {
      "!type": "fn() -> ?",
      "!doc": "Closes the current subpath with a straight line back to where it started.",
      "!params": [],
      "!returns": "One command."
    },
    "polyline": {
      "!type": "fn(points: [?]) -> [?]",
      "!doc": "A series of points: the first is a move, the rest are lines.",
      "!params": [
        {
          "name": "points",
          "type": "[?]",
          "doc": "An array of [x, y] pairs.",
          "required": true
        }
      ],
      "!returns": "One command per point."
    },
    "rect": {
      "!type": "fn(x: number, y: number, width: number, height: number) -> [?]",
      "!doc": "A closed rectangle.",
      "!params": [
        {
          "name": "x",
          "type": "number",
          "doc": "Left edge.",
          "required": true
        },
        {
          "name": "y",
          "type": "number",
          "doc": "Top edge.",
          "required": true
        },
        {
          "name": "width",
          "type": "number",
          "doc": "In viewBox units.",
          "required": true
        },
        {
          "name": "height",
          "type": "number",
          "doc": "In viewBox units.",
          "required": true
        }
      ],
      "!returns": "Five commands: a move, three lines and a close."
    },
    "circle": {
      "!type": "fn(cx: number, cy: number, radius: number) -> [?]",
      "!doc": "A closed circle.",
      "!params": [
        {
          "name": "cx",
          "type": "number",
          "doc": "Centre, horizontal.",
          "required": true
        },
        {
          "name": "cy",
          "type": "number",
          "doc": "Centre, vertical.",
          "required": true
        },
        {
          "name": "radius",
          "type": "number",
          "doc": "In viewBox units.",
          "required": true
        }
      ],
      "!returns": "Two commands: a full arc and a close."
    },
    "d": {
      "!type": "fn(data: string) -> [?]",
      "!doc": "Parses an SVG path data string. Supports M L H V C S Q T A Z, absolute and relative.",
      "!params": [
        {
          "name": "data",
          "type": "string",
          "doc": "The contents of an SVG <path>'s d attribute. Flags written without separators, as an exporter minifies them, parse the same as spaced ones.",
          "required": true
        }
      ],
      "!returns": "One command per segment. Elliptical arcs become one to four cubics each, since nothing below can express an ellipse."
    },
    "!params": [
      {
        "name": "options",
        "type": "?",
        "doc": "",
        "fields": [
          {
            "name": "commands",
            "type": "[?]",
            "doc": "The outline, as an array. Build it with Path.move, Path.line, Path.curve, Path.quad, Path.arc and Path.close, or all at once with Path.d.",
            "required": true
          },
          {
            "name": "viewBox",
            "type": "?",
            "doc": "The coordinate space the commands are written in, as { width, height }. The outline is then fitted to whatever size the widget gives it, so one shape serves every family.",
            "fields": [
              {
                "name": "width",
                "type": "number",
                "doc": "",
                "required": true
              },
              {
                "name": "height",
                "type": "number",
                "doc": "",
                "required": true
              }
            ]
          },
          {
            "name": "fit",
            "type": "BoxFit",
            "doc": "How the outline fills the space. contain by default, which keeps the proportions; fill stretches each axis on its own, which is what a chart wants."
          },
          {
            "name": "fill",
            "type": "+RootlessColor",
            "doc": "Fill colour."
          },
          {
            "name": "fillGradient",
            "type": "+LinearGradient",
            "doc": "Gradient fill. Wins over fill."
          },
          {
            "name": "fillRule",
            "type": "string",
            "doc": "nonZero (default) or evenOdd, for how overlapping subpaths interact."
          },
          {
            "name": "stroke",
            "type": "+RootlessColor",
            "doc": "Outline colour."
          },
          {
            "name": "strokeWidth",
            "type": "number",
            "doc": "Outline thickness."
          },
          {
            "name": "strokeCap",
            "type": "string",
            "doc": "butt (default), round or square, for how a line ends."
          },
          {
            "name": "strokeJoin",
            "type": "string",
            "doc": "miter (default), round or bevel, for how two lines meet."
          },
          {
            "name": "dash",
            "type": "[number]",
            "doc": "A dash pattern, as alternating lengths of line and gap."
          },
          {
            "name": "width",
            "type": "?",
            "doc": "A number of points, or a Size."
          },
          {
            "name": "height",
            "type": "?",
            "doc": "A number of points, or a Size."
          }
        ]
      }
    ]
  },
  "Animated": {
    "!type": "fn(options: ?) -> +WidgetNode",
    "!doc": "Wraps a node with { id, value, animation, transition, contentTransition, child } so an update animates instead of crossfading.",
    "!category": "uiComponents",
    "!subtitle": "How a node changes between renders",
    "!description": "A widget is not running while it is on screen. Its views are rendered in another process, archived, and shown later, so an animation is the difference between two renders and the system plays it. No loops, no timers, nothing longer than two seconds.\n\nid and value do different jobs and both are required. id is stable and marks a view as the same one as last render; without it the tree, rebuilt from scratch every run, looks new and gets crossfaded. That is the usual reason a transition appears to be ignored. value is what changed, and is what fires the animation.\n\nAnimations are switched off on the Always-On display automatically.",
    "!examples": [
      {
        "title": "A number ticking over",
        "code": "var total = Storage.get(\"total\") || 0;\n\nAnimated({\n  id: \"total\",\n  value: total,\n  animation: Animation.spring({ duration: 0.3 }),\n  contentTransition: ContentTransition.numericText(),\n  child: Text(String(total), { font: Font.TITLE }),\n})"
      },
      {
        "title": "A row that slides in when it changes",
        "code": "Animated({\n  id: \"latest:\" + item.id,          // changes when the item does\n  value: item.id,\n  animation: Animation.easeInOut({ duration: 0.4 }),\n  transition: Transition.push(Edge.bottom),\n  child: Text(item.title),\n})"
      }
    ],
    "!params": [
      {
        "name": "options",
        "type": "?",
        "doc": "",
        "fields": [
          {
            "name": "id",
            "type": "string",
            "doc": "Stable across runs. Without it the system treats the view as new and crossfades it, which is why a transition seems ignored.",
            "required": true
          },
          {
            "name": "value",
            "type": "?",
            "doc": "What changed. The animation fires when this differs from last render.",
            "required": true
          },
          {
            "name": "animation",
            "type": "+RootlessAnimation",
            "doc": "Which curve to use."
          },
          {
            "name": "transition",
            "type": "?",
            "doc": "How the view arrives and leaves. Needs id."
          },
          {
            "name": "contentTransition",
            "type": "?",
            "doc": "How a value inside a view that stays changes."
          },
          {
            "name": "child",
            "type": "+WidgetNode",
            "doc": "The node inside."
          }
        ]
      }
    ]
  },
  "Animation": {
    "!doc": "Curves for an Animated node.",
    "!category": "primitives",
    "!subtitle": "Animation curves",
    "!description": "Every curve takes an optional { duration }; spring also takes { bounce }.\n\nTwo seconds is a hard ceiling in a widget and the system cuts anything longer off part-way. Longer durations are clamped and a warning appears in the console.",
    "!examples": [
      {
        "title": "Curves",
        "code": "Animation.spring({ duration: 0.3, bounce: 0.2 })\nAnimation.easeInOut({ duration: 0.4 }).delay(0.1)\nAnimation.linear({ duration: 1 }).speed(2)\nAnimation.none()                       // no animation at all"
      }
    ],
    "default": {
      "!type": "fn(options?: ?) -> +RootlessAnimation",
      "!doc": "The system's standard curve.",
      "!params": [
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "duration",
              "type": "+RootlessDuration",
              "doc": "How long it runs. Clamped to two seconds, the widget ceiling."
            }
          ]
        }
      ]
    },
    "linear": {
      "!type": "fn(options?: ?) -> +RootlessAnimation",
      "!doc": "Constant speed throughout.",
      "!params": [
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "duration",
              "type": "+RootlessDuration",
              "doc": "How long it runs. Clamped to two seconds, the widget ceiling."
            }
          ]
        }
      ]
    },
    "easeIn": {
      "!type": "fn(options?: ?) -> +RootlessAnimation",
      "!doc": "Starts slow, ends at full speed.",
      "!params": [
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "duration",
              "type": "+RootlessDuration",
              "doc": "How long it runs. Clamped to two seconds, the widget ceiling."
            }
          ]
        }
      ]
    },
    "easeOut": {
      "!type": "fn(options?: ?) -> +RootlessAnimation",
      "!doc": "Starts at full speed, ends slow.",
      "!params": [
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "duration",
              "type": "+RootlessDuration",
              "doc": "How long it runs. Clamped to two seconds, the widget ceiling."
            }
          ]
        }
      ]
    },
    "easeInOut": {
      "!type": "fn(options?: ?) -> +RootlessAnimation",
      "!doc": "Slow at both ends.",
      "!params": [
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "duration",
              "type": "+RootlessDuration",
              "doc": "How long it runs. Clamped to two seconds, the widget ceiling."
            }
          ]
        }
      ]
    },
    "bouncy": {
      "!type": "fn(options?: ?) -> +RootlessAnimation",
      "!doc": "A spring that overshoots.",
      "!params": [
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "duration",
              "type": "+RootlessDuration",
              "doc": "How long it runs. Clamped to two seconds, the widget ceiling."
            }
          ]
        }
      ]
    },
    "smooth": {
      "!type": "fn(options?: ?) -> +RootlessAnimation",
      "!doc": "A spring with no overshoot.",
      "!params": [
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "duration",
              "type": "+RootlessDuration",
              "doc": "How long it runs. Clamped to two seconds, the widget ceiling."
            }
          ]
        }
      ]
    },
    "snappy": {
      "!type": "fn(options?: ?) -> +RootlessAnimation",
      "!doc": "A spring with a little overshoot.",
      "!params": [
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "duration",
              "type": "+RootlessDuration",
              "doc": "How long it runs. Clamped to two seconds, the widget ceiling."
            }
          ]
        }
      ]
    },
    "spring": {
      "!type": "fn(options?: ?) -> +RootlessAnimation",
      "!doc": "A spring you tune yourself.",
      "!params": [
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "duration",
              "type": "+RootlessDuration",
              "doc": "How long it runs. Clamped to two seconds."
            },
            {
              "name": "bounce",
              "type": "number",
              "doc": "0 is no overshoot, 1 is very springy. Default 0."
            }
          ]
        }
      ]
    },
    "none": {
      "!type": "fn() -> +RootlessAnimation",
      "!doc": "No animation: the update simply appears."
    }
  },
  "Transition": {
    "!doc": "How a view arrives and leaves: opacity(), slide(), scale(), blurReplace(), identity(), move(edge), push(edge), asymmetric(insertion, removal), combined(a, b).",
    "!category": "primitives",
    "!subtitle": "How a view enters and exits",
    "!description": "Only fires on a node that also carries a stable id. Without one the system treats the view as new and crossfades it.",
    "!examples": [
      {
        "title": "Transitions",
        "code": "Transition.push(Edge.bottom)\nTransition.move(Edge.leading)\nTransition.asymmetric(Transition.push(Edge.bottom), Transition.opacity())"
      }
    ],
    "opacity": {
      "!type": "fn() -> ?",
      "!doc": "Fades in and out."
    },
    "slide": {
      "!type": "fn() -> ?",
      "!doc": "Slides in from the leading edge and out to the trailing one."
    },
    "scale": {
      "!type": "fn() -> ?",
      "!doc": "Grows in and shrinks out."
    },
    "blurReplace": {
      "!type": "fn() -> ?",
      "!doc": "Blurs one out while the other sharpens in."
    },
    "identity": {
      "!type": "fn() -> ?",
      "!doc": "No transition: the view is simply there or not."
    },
    "move": {
      "!type": "fn(edge?: string) -> ?",
      "!doc": "Moves in and out through the given edge.",
      "!params": [
        {
          "name": "edge",
          "type": "Edge",
          "doc": "Which side. Default bottom."
        }
      ]
    },
    "push": {
      "!type": "fn(edge?: string) -> ?",
      "!doc": "Pushes in from the given edge while the old view leaves through the opposite one.",
      "!params": [
        {
          "name": "edge",
          "type": "Edge",
          "doc": "Which side. Default bottom."
        }
      ]
    },
    "asymmetric": {
      "!type": "fn(insertion: ?, removal: ?) -> ?",
      "!doc": "Different transitions for arriving and leaving.",
      "!params": [
        {
          "name": "insertion",
          "type": "?",
          "doc": "Used when the view appears.",
          "required": true
        },
        {
          "name": "removal",
          "type": "?",
          "doc": "Used when it goes away.",
          "required": true
        }
      ]
    },
    "combined": {
      "!type": "fn(first: ?, second: ?) -> ?",
      "!doc": "Runs two transitions together.",
      "!params": [
        {
          "name": "first",
          "type": "?",
          "doc": "One transition.",
          "required": true
        },
        {
          "name": "second",
          "type": "?",
          "doc": "The other.",
          "required": true
        }
      ]
    }
  },
  "ContentTransition": {
    "!doc": "How the contents of a view change in place: numericText(countsDown), interpolate(), opacity(), symbolEffect(), identity().",
    "!category": "primitives",
    "!subtitle": "How contents change in place",
    "!description": "For a value changing inside a view that stays. numericText is for counters and totals; symbolEffect swaps one SF Symbol for another. Transition is the other one, for a whole view arriving or leaving.",
    "!examples": [
      {
        "title": "A counter",
        "code": "Animated({\n  id: \"steps\",\n  value: steps,\n  contentTransition: ContentTransition.numericText(),\n  animation: Animation.snappy(),\n  child: Text(Format.number(steps)),\n})"
      }
    ],
    "numericText": {
      "!type": "fn(countsDown?: bool) -> ?",
      "!doc": "Rolls digits like an odometer. For counters and totals.",
      "!params": [
        {
          "name": "countsDown",
          "type": "bool",
          "doc": "Roll downwards. Default false."
        }
      ]
    },
    "interpolate": {
      "!type": "fn() -> ?",
      "!doc": "Blends between the two states where the system can."
    },
    "opacity": {
      "!type": "fn() -> ?",
      "!doc": "Cross-fades the content."
    },
    "symbolEffect": {
      "!type": "fn() -> ?",
      "!doc": "Swaps one SF Symbol for another with its own effect."
    },
    "identity": {
      "!type": "fn() -> ?",
      "!doc": "No transition: the content simply changes."
    }
  },
  "Edge": {
    "!doc": "top, bottom, leading, trailing, the side a transition moves from.",
    "!category": "enums",
    "!subtitle": "A side of a view",
    "!description": "Used by Transition.move and Transition.push.",
    "!examples": [
      {
        "title": "Edges",
        "code": "Transition.push(Edge.bottom)"
      }
    ],
    "top": {
      "!type": "string",
      "!doc": "The top edge."
    },
    "bottom": {
      "!type": "string",
      "!doc": "The bottom edge."
    },
    "leading": {
      "!type": "string",
      "!doc": "The leading edge."
    },
    "trailing": {
      "!type": "string",
      "!doc": "The trailing edge."
    }
  },
  "LiveActivity": {
    "!doc": "A widget on the Lock Screen and in the Dynamic Island.",
    "!category": "systemAPIs",
    "!subtitle": "Lock Screen and Dynamic Island",
    "!description": "The content is a widget tree, the same one Script.setWidget() takes. A Widget where a background and padding matter, a bare node like Text(\"38m\") where they do not; both work anywhere content is accepted. Three limits come from iOS: starting an activity needs Rootless in the foreground, so a widget refresh or a Shortcut can update and end but not start; all four trees together must encode to under 4KB; and iOS ends an activity after eight hours. Activities are scoped per script, so all() and end() see only your own.",
    "!examples": [
      {
        "title": "Start, update, end",
        "code": "var id = LiveActivity.start({\n  title: \"Laundry\",\n  content: new Widget({ child: Text(\"38 min left\") }),\n  island: {\n    compactLeading: Text(\"🧺\"),\n    compactTrailing: Text(\"38m\"),\n    expanded: {\n      leading: Text(\"🧺 Washing\"),\n      trailing: Text(\"38m\"),\n      bottom: ProgressBar({ value: 0.4 }),\n    },\n  },\n});\n\nLiveActivity.update(id, {\n  content: new Widget({ child: Text(\"2 min left\") }),\n  alert: { title: \"Almost done\", body: \"Two minutes left\" },\n});\n\nLiveActivity.end(id, { dismissAfter: Duration.minutes(5) });"
      },
      {
        "title": "Only when it can work",
        "code": "if (!LiveActivity.areEnabled) {\n  console.warn(\"Live Activities are off for Rootless.\");\n} else {\n  LiveActivity.start({ title: \"Timer\", content: new Widget({ child: Text(\"Go\") }) });\n}"
      }
    ],
    "isSupported": {
      "!type": "bool",
      "!doc": "Whether this device can show Live Activities at all. False on Mac."
    },
    "areEnabled": {
      "!type": "bool",
      "!doc": "Whether the user has left them on for Rootless, in Settings."
    },
    "start": {
      "!type": "fn(options: ?) -> string",
      "!doc": "Starts one and returns its id. Options: title, content (a Widget or a node), island, staleAfter (Duration). island takes compactLeading, compactTrailing and minimal for the collapsed states, plus expanded: {leading, trailing, center, bottom} for the long-pressed one. Leading and trailing flank the camera cutout, center sits under it, bottom spans the full width. Without expanded, the banner is shown across the bottom and the band beside the cutout stays empty. Needs Rootless in the foreground.",
      "!params": [
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "required": true,
          "fields": [
            {
              "name": "title",
              "type": "string",
              "doc": "Names the activity. Not drawn. It identifies it in `all()`."
            },
            {
              "name": "content",
              "type": "?",
              "doc": "The Lock Screen banner: a Widget, or a bare node when a background and padding are not wanted.",
              "required": true
            },
            {
              "name": "island",
              "type": "?",
              "doc": "The Dynamic Island. Leave it out and the island shows nothing.",
              "fields": [
                {
                  "name": "compactLeading",
                  "type": "?",
                  "doc": "Left of the camera cutout when the island is collapsed. About 44pt wide, an icon or a few characters."
                },
                {
                  "name": "compactTrailing",
                  "type": "?",
                  "doc": "Right of the cutout, same size."
                },
                {
                  "name": "minimal",
                  "type": "?",
                  "doc": "A ~36pt circle, shown when another app also has an activity running and yours is squeezed."
                },
                {
                  "name": "expanded",
                  "type": "?",
                  "doc": "The long-pressed island, by region. Without it the banner is shown across the bottom.",
                  "fields": [
                    {
                      "name": "leading",
                      "type": "?",
                      "doc": "Left of the cutout, level with it. Fill this and `trailing` or the band beside the camera stays empty."
                    },
                    {
                      "name": "trailing",
                      "type": "?",
                      "doc": "Right of the cutout."
                    },
                    {
                      "name": "center",
                      "type": "?",
                      "doc": "Under the cutout, between leading and trailing."
                    },
                    {
                      "name": "bottom",
                      "type": "?",
                      "doc": "Full width, beneath everything else."
                    }
                  ]
                }
              ]
            },
            {
              "name": "staleAfter",
              "type": "+RootlessDuration",
              "doc": "How long before what is on screen counts as out of date. The activity stays up; `all()` starts reporting it as \"stale\"."
            }
          ]
        }
      ],
      "!returns": "The activity's id. Pass it to update() and end(). Worth putting in Storage: a later run has no other way back to it."
    },
    "update": {
      "!type": "fn(id: string, options: ?)",
      "!doc": "Replaces the content. Options: content, island, staleAfter, and alert ({title, body}) to make the update ring.",
      "!params": [
        {
          "name": "id",
          "type": "string",
          "doc": "From start(), or from all().",
          "required": true
        },
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "required": true,
          "fields": [
            {
              "name": "content",
              "type": "?",
              "doc": "Replaces the banner.",
              "required": true
            },
            {
              "name": "island",
              "type": "?",
              "doc": "Replaces the island. Same shape as in start().",
              "fields": [
                {
                  "name": "compactLeading",
                  "type": "?",
                  "doc": "Left of the camera cutout when the island is collapsed. About 44pt wide, an icon or a few characters."
                },
                {
                  "name": "compactTrailing",
                  "type": "?",
                  "doc": "Right of the cutout, same size."
                },
                {
                  "name": "minimal",
                  "type": "?",
                  "doc": "A ~36pt circle, shown when another app also has an activity running and yours is squeezed."
                },
                {
                  "name": "expanded",
                  "type": "?",
                  "doc": "By region, as in start().",
                  "fields": [
                    {
                      "name": "leading",
                      "type": "?",
                      "doc": "Left of the cutout, level with it. Fill this and `trailing` or the band beside the camera stays empty."
                    },
                    {
                      "name": "trailing",
                      "type": "?",
                      "doc": "Right of the cutout."
                    },
                    {
                      "name": "center",
                      "type": "?",
                      "doc": "Under the cutout, between leading and trailing."
                    },
                    {
                      "name": "bottom",
                      "type": "?",
                      "doc": "Full width, beneath everything else."
                    }
                  ]
                }
              ]
            },
            {
              "name": "staleAfter",
              "type": "+RootlessDuration",
              "doc": "Resets the staleness clock."
            },
            {
              "name": "alert",
              "type": "?",
              "doc": "Makes the update ring and light up the screen. Leave it out for a silent one.",
              "fields": [
                {
                  "name": "title",
                  "type": "string",
                  "doc": "Shown on devices that cannot display the activity itself, like a Watch."
                },
                {
                  "name": "body",
                  "type": "string",
                  "doc": "The line under it."
                }
              ]
            }
          ]
        }
      ]
    },
    "end": {
      "!type": "fn(id: string, options: ?)",
      "!doc": "Ends it. Options: content for a final frame, dismissAfter (Duration) to leave it on screen a while longer.",
      "!params": [
        {
          "name": "id",
          "type": "string",
          "doc": "From start(), or from all().",
          "required": true
        },
        {
          "name": "options",
          "type": "?",
          "doc": "Optional, end(id) keeps whatever is on screen as the last frame.",
          "fields": [
            {
              "name": "content",
              "type": "?",
              "doc": "A final frame, if the last state is not the one to leave behind."
            },
            {
              "name": "dismissAfter",
              "type": "+RootlessDuration",
              "doc": "Keeps it on the Lock Screen this much longer before it disappears. Default is the system's own timing."
            }
          ]
        }
      ]
    },
    "all": {
      "!type": "fn() -> [?]",
      "!doc": "This script's running activities.",
      "!returns": "One { id, title, state } per activity. state is \"active\", \"stale\" (still on screen but past staleAfter, so what it shows is out of date), \"ended\" or \"dismissed\". An activity the user swiped away is not in the list at all, so an empty result means the session is over."
    },
    "endAll": {
      "!type": "fn()",
      "!doc": "Ends every activity this script started, immediately."
    }
  },
  "Vision": {
    "!doc": "Text, barcodes and faces in an image.",
    "!category": "systemAPIs",
    "!subtitle": "On-device image understanding",
    "!description": "Works on the image bytes you pass in, so it needs no permission and no network. Images come from Image or from a share extension input. Rectangles are normalised 0 to 1 with the origin at the top left, not Vision's bottom left.",
    "!examples": [
      {
        "title": "Text out of a screenshot",
        "code": "var shot = Image.file(\"/path/to/shot.png\");\nvar lines = Vision.text(shot);\n\nScript.setWidget(new Widget({\n  child: Column({\n    children: lines.slice(0, 4).map(function (l) { return Text(l.text); }),\n  }),\n}));"
      }
    ],
    "text": {
      "!type": "fn(image: ?, options: ?) -> [?]",
      "!doc": "Every line of text found, as { text, confidence, x, y, width, height }.",
      "!params": [
        {
          "name": "image",
          "type": "?",
          "doc": "From Image.file, Image.download, Canvas.render, or a share extension input.",
          "required": true
        },
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "languages",
              "type": "[string]",
              "doc": "BCP-47 codes to prefer, like [\"it-IT\", \"en-US\"]. Left out, Vision decides."
            },
            {
              "name": "fast",
              "type": "bool",
              "doc": "Trades accuracy for speed. Worth it in a widget, where the budget is short."
            }
          ]
        }
      ],
      "!returns": "One entry per line, in the order Vision found them, not top to bottom."
    },
    "barcodes": {
      "!type": "fn(image: ?) -> [?]",
      "!doc": "Barcodes and QR codes, as { payload, symbology, x, y, width, height }.",
      "!params": [
        {
          "name": "image",
          "type": "?",
          "doc": "The image to scan.",
          "required": true
        }
      ],
      "!returns": "symbology is the bare name, \"QR\" or \"EAN13\", not Vision's prefixed constant."
    },
    "faces": {
      "!type": "fn(image: ?) -> [?]",
      "!doc": "Face rectangles, as { x, y, width, height, roll, yaw }.",
      "!params": [
        {
          "name": "image",
          "type": "?",
          "doc": "The image to scan.",
          "required": true
        }
      ],
      "!returns": "roll and yaw are radians, and 0 when Vision could not tell."
    }
  },
  "Language": {
    "!doc": "Language identification, sentiment, tokenisation and entities.",
    "!category": "systemAPIs",
    "!subtitle": "Language, sentiment, entities",
    "!description": "Runs on device. No permission, no network, fast enough to call from a widget.",
    "!examples": [
      {
        "title": "Colour a headline by mood",
        "code": "var score = Language.sentiment(headline);\nvar colour = score > 0.2 ? Color.green() : score < -0.2 ? Color.red() : Color.gray();\nScript.setWidget(new Widget({ child: Text(headline, { color: colour }) }));"
      }
    ],
    "detect": {
      "!type": "fn(text: string) -> ?",
      "!doc": "The dominant language, or null when the text is too short.",
      "!params": [
        {
          "name": "text",
          "type": "string",
          "doc": "The text to identify.",
          "required": true
        }
      ],
      "!returns": "code is a language code like \"it\" or \"en\"."
    },
    "sentiment": {
      "!type": "fn(text: string) -> number",
      "!doc": "-1 for negative, 1 for positive, 0 for neutral.",
      "!params": [
        {
          "name": "text",
          "type": "string",
          "doc": "The text to score.",
          "required": true
        }
      ],
      "!returns": "0 means neutral, and is also what a language with no sentiment model returns. The two cases are indistinguishable."
    },
    "tokenize": {
      "!type": "fn(text: string, unit: string) -> [string]",
      "!doc": "Splits text into words, sentences or paragraphs.",
      "!params": [
        {
          "name": "text",
          "type": "string",
          "doc": "The text to split.",
          "required": true
        },
        {
          "name": "unit",
          "type": "string",
          "doc": "\"word\", \"sentence\" or \"paragraph\". Defaults to word."
        }
      ]
    },
    "entities": {
      "!type": "fn(text: string) -> [?]",
      "!doc": "People, places and organisations, as { text, type }.",
      "!params": [
        {
          "name": "text",
          "type": "string",
          "doc": "The text to scan.",
          "required": true
        }
      ]
    },
    "lemmas": {
      "!type": "fn(text: string) -> [?]",
      "!doc": "Each word with its dictionary form, as { text, lemma }.",
      "!params": [
        {
          "name": "text",
          "type": "string",
          "doc": "The text to analyse.",
          "required": true
        }
      ]
    }
  },
  "Photos": {
    "!doc": "Reads the photo library.",
    "!category": "systemAPIs",
    "!subtitle": "The photo library",
    "!description": "requestAccess() works only in the app: a widget cannot show a permission sheet. iOS also offers limited access, where the user picks which photos an app can see. status reports it separately and reads simply return fewer assets. image() takes a maxSize because a widget is killed above roughly 30MB, and a full-size photo decodes to more than that.",
    "!examples": [
      {
        "title": "A random photo, every hour",
        "code": "if (!Photos.isAuthorized) {\n  Script.setWidget(new Widget({ child: Text(\"Grant photo access in the app\") }));\n} else {\n  var pick = Photos.random();\n  Script.setWidget(new Widget({\n    refreshInterval: Duration.hours(1),\n    child: Image.from(Photos.image(pick, { maxSize: 600 }), { fit: BoxFit.cover }),\n  }));\n}"
      }
    ],
    "status": {
      "!type": "string",
      "!doc": "\"authorized\", \"limited\", \"denied\", \"restricted\" or \"notDetermined\"."
    },
    "isAuthorized": {
      "!type": "bool",
      "!doc": "True for authorized and for limited."
    },
    "requestAccess": {
      "!type": "fn() -> string",
      "!doc": "Shows the permission sheet. App only.",
      "!returns": "The status afterwards."
    },
    "albums": {
      "!type": "fn() -> [?]",
      "!doc": "Albums holding at least one item. Smart albums such as Favourites and Screenshots come first.",
      "!returns": "One { id, title, count } per album. Pass id to latest() or random()."
    },
    "latest": {
      "!type": "fn(options: ?) -> [?]",
      "!doc": "The newest assets first.",
      "!params": [
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "count",
              "type": "number",
              "doc": "How many. Defaults to 10."
            },
            {
              "name": "album",
              "type": "string",
              "doc": "An album id from albums(). Left out, the whole library."
            },
            {
              "name": "type",
              "type": "string",
              "doc": "\"image\" (default), \"video\" or \"any\"."
            }
          ]
        }
      ],
      "!returns": "Each asset is { id, width, height, type, isFavorite, createdAt, latitude?, longitude? }."
    },
    "random": {
      "!type": "fn(options: ?) -> ?",
      "!doc": "One asset at random, or null if there are none.",
      "!params": [
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "album",
              "type": "string",
              "doc": "An album id to pick from."
            },
            {
              "name": "type",
              "type": "string",
              "doc": "\"image\" (default), \"video\" or \"any\"."
            }
          ]
        }
      ],
      "!returns": "One asset, shaped as in latest(), or null when nothing matches."
    },
    "image": {
      "!type": "fn(asset: ?, options: ?) -> ?",
      "!doc": "Loads the pixels. Downloads from iCloud if the asset is not on the device.",
      "!params": [
        {
          "name": "asset",
          "type": "?",
          "doc": "An asset from latest() or random(), or just its id.",
          "required": true
        },
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "maxSize",
              "type": "number",
              "doc": "Longest edge in pixels. Default 1024."
            }
          ]
        }
      ],
      "!returns": "An image, ready for an Image node or for Vision."
    }
  },
  "Contacts": {
    "!doc": "Names, phone numbers, emails and birthdays.",
    "!category": "systemAPIs",
    "!subtitle": "Names, numbers, birthdays",
    "!description": "requestAccess() works only in the app, like Photos. iOS 18 adds limited access, where the user picks which contacts an app can see; status reports it separately. birthdays() works out the days remaining with Calendar rather than by subtracting, so leap days and the year rollover come out right. age is absent when the birthday was stored without a year, which is common.",
    "!examples": [
      {
        "title": "Who is next",
        "code": "var soon = Contacts.birthdays({ withinDays: 30 });\n\nScript.setWidget(new Widget({\n  child: Column({\n    crossAxisAlignment: CrossAxisAlignment.start,\n    children: soon.slice(0, 3).map(function (p) {\n      var when = p.daysUntil === 0 ? \"today\" : \"in \" + p.daysUntil + \" days\";\n      return Text(p.name + \", \" + when);\n    }),\n  }),\n}));"
      }
    ],
    "status": {
      "!type": "string",
      "!doc": "\"authorized\", \"limited\", \"denied\", \"restricted\" or \"notDetermined\"."
    },
    "isAuthorized": {
      "!type": "bool",
      "!doc": "True for authorized and for limited."
    },
    "requestAccess": {
      "!type": "fn() -> string",
      "!doc": "Shows the permission sheet. App only.",
      "!returns": "The status afterwards."
    },
    "all": {
      "!type": "fn(options: ?) -> [?]",
      "!doc": "Every contact, by given name.",
      "!params": [
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "limit",
              "type": "number",
              "doc": "Stop after this many. 0 or absent means all of them."
            }
          ]
        }
      ],
      "!returns": "Each is { id, name, givenName, familyName, nickname?, organization?, phones, emails, birthday?, hasImage }."
    },
    "search": {
      "!type": "fn(name: string, options: ?) -> [?]",
      "!doc": "Contacts whose name matches. Uses the system index.",
      "!params": [
        {
          "name": "name",
          "type": "string",
          "doc": "Part of a name.",
          "required": true
        },
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "limit",
              "type": "number",
              "doc": "Stop after this many."
            }
          ]
        }
      ]
    },
    "birthdays": {
      "!type": "fn(options: ?) -> [?]",
      "!doc": "Upcoming birthdays, soonest first.",
      "!params": [
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "withinDays",
              "type": "number",
              "doc": "How far ahead to look. Defaults to 365."
            }
          ]
        }
      ],
      "!returns": "A contact plus daysUntil, where 0 means today, and age. age is absent when the birthday has no year."
    },
    "image": {
      "!type": "fn(contact: ?, options: ?) -> ?",
      "!doc": "The contact's picture, or null. Most contacts have none.",
      "!params": [
        {
          "name": "contact",
          "type": "?",
          "doc": "A contact or its id.",
          "required": true
        },
        {
          "name": "options",
          "type": "?",
          "doc": "",
          "fields": [
            {
              "name": "thumbnail",
              "type": "bool",
              "doc": "Small version. True by default; the full one is worth it only in the app."
            }
          ]
        }
      ],
      "!returns": "An image, ready for an Image node or for Vision, or null."
    }
  },
  "Chart": {
    "!type": "fn(options?: ?) -> +WidgetNode",
    "!doc": "A chart built from one or more marks.",
    "!category": "uiComponents",
    "!subtitle": "Bars, lines, areas and points",
    "!description": "Marks share one pair of axes, so a line over bars is one Chart with two marks. What x is decides the axis: numbers give a numeric axis, strings a category axis, DateTimes a date axis. It is worked out from the data, and xType overrides it. Mixing kinds in one chart is an error, because no axis can hold both.\n\nGive a point a `series` and colour stops being a property of the mark and becomes a mapping from the data: points sharing a series share a colour, bars sitting on the same x sit side by side, and a legend appears. A mark's `name` sets the series for all of its points at once. Without either, a mark is one flat `color`, as before.\n\nIn a widget a chart redraws when the timeline does, not continuously. Wrap it in Animated to move between two sets of data.",
    "!examples": [
      {
        "title": "Bars with an average line",
        "code": "var week = [\n  { x: \"Mon\", y: 5 }, { x: \"Tue\", y: 8 }, { x: \"Wed\", y: 3 },\n  { x: \"Thu\", y: 9 }, { x: \"Fri\", y: 6 },\n];\nvar mean = week.reduce(function (t, d) { return t + d.y; }, 0) / week.length;\n\nScript.setWidget(new Widget({\n  padding: EdgeInsets.all(12),\n  child: Chart({\n    marks: [\n      Bar({ data: week, color: Color.blue() }),\n      Rule({ value: mean, color: Color.orange() }),\n    ],\n    xAxis: { grid: false },\n    yAxis: { ticks: 3 },\n  }),\n}));"
      },
      {
        "title": "Bars grouped by series",
        "code": "var sales = [\n  { x: \"Mon\", y: 5, series: \"North\" }, { x: \"Mon\", y: 3, series: \"South\" },\n  { x: \"Tue\", y: 8, series: \"North\" }, { x: \"Tue\", y: 6, series: \"South\" },\n  { x: \"Wed\", y: 4, series: \"North\" }, { x: \"Wed\", y: 7, series: \"South\" },\n];\n\nScript.setWidget(new Widget({\n  padding: EdgeInsets.all(12),\n  child: Chart({\n    marks: [Bar({ data: sales })],\n    colors: [Color.blue(), Color.orange()],\n  }),\n}));"
      }
    ],
    "!params": [
      {
        "name": "options",
        "type": "?",
        "doc": "",
        "fields": [
          {
            "name": "marks",
            "type": "[?]",
            "doc": "Bar, Line, Area, Point and Rule, drawn in the order given.",
            "required": true
          },
          {
            "name": "colors",
            "type": "[Color]",
            "doc": "The colour scale, in the order series are first seen. Left out, the system chooses. Only read when a mark uses series."
          },
          {
            "name": "xAxis",
            "type": "?",
            "doc": "",
            "fields": [
              {
                "name": "visible",
                "type": "bool",
                "doc": "Draw the axis at all. Default true."
              },
              {
                "name": "grid",
                "type": "bool",
                "doc": "Grid lines at each tick. Default true."
              },
              {
                "name": "labels",
                "type": "bool",
                "doc": "Text at each tick. Default true."
              },
              {
                "name": "ticks",
                "type": "number",
                "doc": "How many ticks to aim for. The system still decides the final positions."
              },
              {
                "name": "domain",
                "type": "[number]",
                "doc": "[min, max]. Left out, the data decides."
              }
            ]
          },
          {
            "name": "yAxis",
            "type": "?",
            "doc": "",
            "fields": [
              {
                "name": "visible",
                "type": "bool",
                "doc": "Draw the axis at all. Default true."
              },
              {
                "name": "grid",
                "type": "bool",
                "doc": "Grid lines at each tick. Default true."
              },
              {
                "name": "labels",
                "type": "bool",
                "doc": "Text at each tick. Default true."
              },
              {
                "name": "ticks",
                "type": "number",
                "doc": "How many ticks to aim for. The system still decides the final positions."
              },
              {
                "name": "domain",
                "type": "[number]",
                "doc": "[min, max]. Left out, the data decides."
              }
            ]
          },
          {
            "name": "xType",
            "type": "string",
            "doc": "number, category or date. Overrides what the data suggests."
          },
          {
            "name": "width",
            "type": "?",
            "doc": "A number of points, or a Size."
          },
          {
            "name": "height",
            "type": "?",
            "doc": "A number of points, or a Size."
          }
        ]
      }
    ]
  },
  "Bar": {
    "!type": "fn(options: ?) -> ?",
    "!doc": "One bar per point.",
    "!category": "uiComponents",
    "!subtitle": "A bar mark",
    "!description": "One bar per point. Goes in a Chart's marks.",
    "!params": [
      {
        "name": "options",
        "type": "?",
        "doc": "",
        "required": true,
        "fields": [
          {
            "name": "data",
            "type": "[?]",
            "doc": "The points, as [{ x, y, series }]. x is a number, a string or a DateTime; y is always a number. series is optional: it is the colour channel, and points that share one share a colour and get a legend entry.",
            "required": true
          },
          {
            "name": "name",
            "type": "string",
            "doc": "The series every point of this mark belongs to, for when they all belong to the same one. A point's own series wins over it."
          },
          {
            "name": "color",
            "type": "+RootlessColor",
            "doc": "Defaults to the accent colour. Ignored once a series is in play, where the colour comes from the chart's scale instead."
          },
          {
            "name": "opacity",
            "type": "number",
            "doc": "0 to 1."
          }
        ]
      }
    ]
  },
  "Line": {
    "!type": "fn(options: ?) -> ?",
    "!doc": "A line through the points.",
    "!category": "uiComponents",
    "!subtitle": "A line mark",
    "!description": "A line through the points. Goes in a Chart's marks.",
    "!params": [
      {
        "name": "options",
        "type": "?",
        "doc": "",
        "required": true,
        "fields": [
          {
            "name": "data",
            "type": "[?]",
            "doc": "The points, as [{ x, y, series }]. x is a number, a string or a DateTime; y is always a number. series is optional: it is the colour channel, and points that share one share a colour and get a legend entry.",
            "required": true
          },
          {
            "name": "name",
            "type": "string",
            "doc": "The series every point of this mark belongs to, for when they all belong to the same one. A point's own series wins over it."
          },
          {
            "name": "color",
            "type": "+RootlessColor",
            "doc": "Defaults to the accent colour. Ignored once a series is in play, where the colour comes from the chart's scale instead."
          },
          {
            "name": "opacity",
            "type": "number",
            "doc": "0 to 1."
          },
          {
            "name": "lineWidth",
            "type": "number",
            "doc": "Thickness. Default 2."
          },
          {
            "name": "interpolation",
            "type": "string",
            "doc": "linear (default), curve, monotone, step, stepStart, stepCenter or cardinal."
          }
        ]
      }
    ]
  },
  "Area": {
    "!type": "fn(options: ?) -> ?",
    "!doc": "The region between the line and the baseline.",
    "!category": "uiComponents",
    "!subtitle": "An area mark",
    "!description": "The region between the line and the baseline. Goes in a Chart's marks.",
    "!params": [
      {
        "name": "options",
        "type": "?",
        "doc": "",
        "required": true,
        "fields": [
          {
            "name": "data",
            "type": "[?]",
            "doc": "The points, as [{ x, y, series }]. x is a number, a string or a DateTime; y is always a number. series is optional: it is the colour channel, and points that share one share a colour and get a legend entry.",
            "required": true
          },
          {
            "name": "name",
            "type": "string",
            "doc": "The series every point of this mark belongs to, for when they all belong to the same one. A point's own series wins over it."
          },
          {
            "name": "color",
            "type": "+RootlessColor",
            "doc": "Defaults to the accent colour. Ignored once a series is in play, where the colour comes from the chart's scale instead."
          },
          {
            "name": "opacity",
            "type": "number",
            "doc": "0 to 1. Default 0.25, so a line drawn over it stays visible."
          },
          {
            "name": "interpolation",
            "type": "string",
            "doc": "linear (default), curve, monotone, step, stepStart, stepCenter or cardinal."
          }
        ]
      }
    ]
  },
  "Point": {
    "!type": "fn(options: ?) -> ?",
    "!doc": "A symbol at each point.",
    "!category": "uiComponents",
    "!subtitle": "A point mark",
    "!description": "A symbol at each point. Goes in a Chart's marks.",
    "!params": [
      {
        "name": "options",
        "type": "?",
        "doc": "",
        "required": true,
        "fields": [
          {
            "name": "data",
            "type": "[?]",
            "doc": "The points, as [{ x, y, series }]. x is a number, a string or a DateTime; y is always a number. series is optional: it is the colour channel, and points that share one share a colour and get a legend entry.",
            "required": true
          },
          {
            "name": "name",
            "type": "string",
            "doc": "The series every point of this mark belongs to, for when they all belong to the same one. A point's own series wins over it."
          },
          {
            "name": "color",
            "type": "+RootlessColor",
            "doc": "Defaults to the accent colour. Ignored once a series is in play, where the colour comes from the chart's scale instead."
          },
          {
            "name": "opacity",
            "type": "number",
            "doc": "0 to 1."
          },
          {
            "name": "symbolSize",
            "type": "number",
            "doc": "Area of the symbol. Default 40."
          }
        ]
      }
    ]
  },
  "Rule": {
    "!type": "fn(options: ?) -> ?",
    "!doc": "A dashed line across the chart: a target, an average, a threshold.",
    "!category": "uiComponents",
    "!subtitle": "A reference line",
    "!description": "A dashed line across the chart: a target, an average, a threshold. Goes in a Chart's marks.",
    "!params": [
      {
        "name": "options",
        "type": "?",
        "doc": "",
        "required": true,
        "fields": [
          {
            "name": "value",
            "type": "number",
            "doc": "Where it sits.",
            "required": true
          },
          {
            "name": "axis",
            "type": "string",
            "doc": "\"y\" for a horizontal line (default), \"x\" for a vertical one."
          },
          {
            "name": "color",
            "type": "+RootlessColor",
            "doc": "Defaults to the accent colour."
          },
          {
            "name": "lineWidth",
            "type": "number",
            "doc": "Thickness. Default 1."
          }
        ]
      }
    ]
  },
  "Sector": {
    "!type": "fn(options: ?) -> ?",
    "!doc": "A pie or a donut: one slice per value, sized by its share of the total.",
    "!category": "uiComponents",
    "!subtitle": "A slice of a pie",
    "!description": "A pie or a donut. Goes in a Chart's marks, and takes the same data as every other mark: x names the slice, y is how big it is. A chart holding a Sector draws no axes, because a slice has an angle rather than a position.",
    "!examples": [
      {
        "title": "A donut of this week's categories",
        "code": "Chart({\n  marks: [Sector({\n    data: [\n      { x: \"Food\", y: 420 },\n      { x: \"Transport\", y: 180 },\n      { x: \"Rent\", y: 900 },\n    ],\n    innerRadius: 0.6,\n    cornerRadius: 4,\n  })],\n})"
      }
    ],
    "!params": [
      {
        "name": "options",
        "type": "?",
        "doc": "",
        "required": true,
        "fields": [
          {
            "name": "data",
            "type": "[?]",
            "doc": "The slices, as { x, y }. x names the slice and labels it, y is its size.",
            "required": true
          },
          {
            "name": "innerRadius",
            "type": "number",
            "doc": "A fraction of the circle, 0 to 1. Above 0 it becomes a donut. Default 0."
          },
          {
            "name": "outerRadius",
            "type": "number",
            "doc": "A fraction of the circle, 0 to 1. Default 1."
          },
          {
            "name": "angularInset",
            "type": "number",
            "doc": "The gap between slices, in points. Default 1."
          },
          {
            "name": "cornerRadius",
            "type": "number",
            "doc": "Rounds the corners of each slice. Default 0."
          },
          {
            "name": "opacity",
            "type": "number",
            "doc": "0 to 1. Default 1."
          }
        ]
      }
    ]
  }
}
