{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://zinejs.dev/options.schema.json",
  "title": "ZineOptions",
  "description": "Constructor options for the Zine flipbook: new Zine(container, options).",
  "type": "object",
  "required": ["source"],
  "additionalProperties": false,
  "properties": {
    "source": {
      "type": "object",
      "description": "Content source resolving page indices to rasters, e.g. new ImageSource(urls). Required; a runtime object, not JSON-serializable."
    },
    "width": {
      "type": "number",
      "minimum": 1,
      "description": "Fixed container width in px; omit to let the container/CSS drive the size."
    },
    "height": {
      "type": "number",
      "minimum": 1,
      "description": "Fixed container height in px; omit to let the container/CSS drive the size."
    },
    "renderer": {
      "description": "Which renderer to use. Default 'auto' (prefer GPU, else CSS).",
      "default": "auto",
      "oneOf": [
        { "type": "string", "enum": ["auto", "css", "webgl2"] },
        {
          "type": "array",
          "items": { "type": "string", "enum": ["css", "webgl2"] },
          "description": "Ordered fallback list; the first available renderer wins."
        },
        { "type": "object", "description": "A custom Renderer instance." }
      ]
    },
    "direction": {
      "type": "string",
      "enum": ["ltr", "rtl"],
      "default": "ltr",
      "description": "Reading direction."
    },
    "spreadMode": {
      "type": "string",
      "enum": ["double", "single", "cover", "book"],
      "default": "cover",
      "description": "How pages group into spreads: 'double' (paired from page 0), 'single' (one per spread), 'cover' (lone first page), 'book' (lone first and last)."
    },
    "curl": {
      "type": "string",
      "enum": ["cone", "simple"],
      "default": "cone",
      "description": "Page-curl model for the WebGL2 renderer. Only the bundled names are valid JSON: 'cone' (natural conical curl, PARC/iBooks-style, anchored at tap; default) and 'simple' (plain flat page turn, no bend). Four more are shipped as importable models rather than names, so a book carries only the curl it uses: 'roll' (roll up then flop over; a lone page turns the same way and dissolves as it lands), 'leaf' (traveling smooth-curvature wave with flat paper before and after the bend, anchored at tap), 'flick' (inertial follow-through: the sheet trails the turn, then swings past and settles, anchored at tap), 'silk' (hand-turned S-curve: spine-driven rotation with body bend + free-edge reverse curl forming a true inflection, early peel lead, anchored at tap). In code, import one from '@zinejs/core/curls' and pass the model; a custom model of the same shape works too."
    },
    "frontCover": {
      "type": "string",
      "description": "Image URL prepended as a lone front cover (adds a page)."
    },
    "backCover": {
      "type": "string",
      "description": "Image URL appended as a lone back cover (adds a page)."
    },
    "pages": {
      "type": "object",
      "description": "Replace source pages with image URLs, keyed by 0-based source index (negative = from the end).",
      "additionalProperties": { "type": "string" }
    },
    "startPage": {
      "type": "integer",
      "minimum": 0,
      "default": 0,
      "description": "Zero-based page to open on; must be within the source's page count."
    },
    "flipDuration": {
      "type": "number",
      "minimum": 0,
      "default": 800,
      "description": "Flip animation duration in milliseconds. Timed for a two-page spread; a lone page stretches this and eases out harder (no facing half to land on)."
    },
    "clickToFlip": {
      "type": "string",
      "enum": ["edge", "half", "off"],
      "default": "edge",
      "description": "How a tap/click turns pages: near an edge, by page half, or off."
    },
    "fit": {
      "type": "string",
      "enum": ["contain", "fill"],
      "default": "contain",
      "description": "How a page whose size differs from the book fits: contain keeps its aspect and centers it (a margin shows); fill stretches it. The book keeps one shape document-wide, so mixed-size pages never shift layout."
    },
    "gutterOverlap": {
      "oneOf": [
        { "const": "auto" },
        { "type": "number", "minimum": 0, "maximum": 0.5 }
      ],
      "default": 0,
      "description": "Hide a print-gutter duplicate (artwork repeated on both sides of the spine for binding). A number is the overlap as a fraction of page width, removed from every two-page spread, half from each page. 'auto' finds the repeat per spread and only trims spreads that have one. Lone pages are never trimmed. 0 = off."
    },
    "strings": {
      "type": "object",
      "description": "Override any reader-facing text (control labels, screen-reader announcements, panel and loading text) for localization. English by default; pass only the keys to change. Interpolated entries are functions, so this is set in code, not JSON."
    },
    "clickZoneSize": {
      "type": "number",
      "minimum": 0,
      "default": 64,
      "description": "Edge-zone size in px per side, used when clickToFlip is 'edge'."
    },
    "cursorHints": {
      "type": "boolean",
      "default": true,
      "description": "On a mouse, change the cursor over the book to hint what a press would do: pointer where a click turns the page, zoom-in where a double-click zooms (including the edge peel band, so a sweep stays calm), grab over the whole page once zoomed (a drag pans), and grabbing while a peel or pan is in flight. No effect on touch."
    },
    "hints": {
      "default": true,
      "description": "Subtle one-shot discoverability hints on the book itself (reaching touch readers, unlike cursorHints): the leading corner peeks on first open, nudges again once if the reader sits idle without turning a page, a 'drag to move' caption appears on first zoom, and (on a mouse) a 'double-click or Ctrl/Cmd-scroll to zoom' caption appears if a lone click lands in a dead zone where only a double-click would do anything. Each stops for good once the reader performs the gesture it teaches. Honors prefers-reduced-motion. true (default) enables them in-memory; { persist: true } records what the reader learned in localStorage so a returning reader is not re-taught.",
      "oneOf": [
        { "type": "boolean" },
        {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "enabled": { "type": "boolean", "default": true, "description": "Whether the hints are shown." },
            "persist": { "type": "boolean", "default": false, "description": "Remember learned gestures across visits via localStorage, instead of per page load." }
          }
        }
      ]
    },
    "sound": {
      "default": false,
      "description": "Play a short sound on each page turn. Off by default (opt-in). true uses the bundled clip at default volume; an object overrides it (url, volume, muted to start silent, persist to remember the reader's mute choice). Playback is best-effort and never throws; a mute control appears in the toolbar and right-click menu whenever sound is enabled (audible or muted).",
      "oneOf": [
        { "type": "boolean" },
        {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "url": { "type": "string", "description": "A clip URL that overrides the bundled default (fetched at runtime)." },
            "volume": { "type": "number", "minimum": 0, "maximum": 1, "default": 0.5, "description": "Playback volume, 0..1." },
            "muted": { "type": "boolean", "default": false, "description": "Offer the sound but start it silent, so the reader enables it via the mute control." },
            "persist": { "type": "boolean", "default": false, "description": "Remember the reader's mute choice across visits via localStorage, instead of resetting each load." }
          }
        }
      ]
    },
    "clickFlipDelay": {
      "type": "number",
      "minimum": 0,
      "description": "Delay (ms) before a click flips, to let a double-click zoom preempt it. Omit for auto (0 when double-click zoom is inactive, 250 when active)."
    },
    "zoom": {
      "type": "object",
      "description": "Zoom behavior.",
      "additionalProperties": false,
      "properties": {
        "enabled": { "type": "boolean", "default": true, "description": "Whether zooming is allowed." },
        "max": { "type": "number", "minimum": 1, "default": 4, "description": "Maximum zoom scale." },
        "wheel": { "type": "boolean", "default": true, "description": "Zoom on Ctrl/Cmd + wheel." },
        "doubleClick": {
          "default": [1, 2, 4],
          "description": "Zoom levels cycled by double-click (wraps); false to disable.",
          "oneOf": [
            { "type": "boolean", "enum": [false] },
            { "type": "array", "items": { "type": "number", "minimum": 1 } }
          ]
        },
        "doubleClickInFlipZone": {
          "type": "boolean",
          "description": "Listen for double-click zoom inside click-to-flip zones. Default: off in 'edge' mode, on in 'half'. Enabling it makes clicks wait out clickFlipDelay."
        }
      }
    },
    "singlePageThreshold": {
      "type": "number",
      "minimum": 0,
      "default": 640,
      "description": "Container widths (px) below this switch to one page per spread; 0 disables the responsive fallback."
    },
    "deepLink": {
      "type": "boolean",
      "default": true,
      "description": "Keep the current page in the URL hash (#page=12), so a link opens where the reader was. Only the 'page' key is touched."
    },
    "responsiveSpread": {
      "type": "boolean",
      "default": true,
      "description": "Whether a narrow container may override spreadMode and show one page at a time. False holds the configured mode at every width."
    },
    "disableContextMenu": {
      "type": "boolean",
      "default": false,
      "description": "Suppress the browser's right-click menu over the book. A deterrent, not protection: the pages remain in the DOM."
    },
    "controls": {
      "default": true,
      "description": "Built-in toolbar. true for the default set, false for none, or an object to configure it.",
      "oneOf": [
        { "type": "boolean" },
        {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "position": {
              "type": "string",
              "enum": ["top", "bottom", "left", "right"],
              "default": "bottom",
              "description": "Which edge of the book the toolbar sits on."
            },
            "docked": {
              "type": "boolean",
              "default": true,
              "description": "Sit outside the book in normal flow; false floats the toolbar over it."
            },
            "items": {
              "type": "array",
              "description": "Toolbar layout: control ids, '|' separators, or inline control definitions. Omit for the default set.",
              "items": { "type": ["string", "object"] }
            },
            "arrows": {
              "type": "boolean",
              "default": true,
              "description": "Large page-turn arrows flanking the book."
            },
            "className": {
              "type": "string",
              "description": "Extra class on the toolbar root, for styling."
            }
          }
        }
      ]
    },
    "hideControls": {
      "type": "array",
      "items": { "type": "string" },
      "description": "Control ids to hide from both surfaces at once: off the toolbar (bar and overflow menu) and out of the right-click menu. E.g. ['print', 'download'] removes those two everywhere. A blunt cross-cutting filter that only removes; to rearrange or add controls, set the per-surface items instead."
    },
    "contextMenu": {
      "default": true,
      "description": "A right-click menu of reading controls, opened at the cursor. On by default. true for the default set, false to keep the browser's own menu, or an object to configure it. Turned off when disableContextMenu is set; setting both explicitly throws.",
      "oneOf": [
        { "type": "boolean" },
        {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "items": {
              "type": "array",
              "description": "Menu layout: control ids, '|' separators, or inline control definitions. Omit for the default set.",
              "items": { "type": ["string", "object"] }
            },
            "colorScheme": {
              "type": "string",
              "enum": ["light", "dark", "auto"],
              "default": "auto",
              "description": "Which palette the menu uses; follows the toolbar's when mounted alongside one."
            }
          }
        }
      ]
    }
  }
}
