{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://icone.mssfpp.dev/schema/icon.schema.json",
  "title": "File .icon",
  "description": "Formato di scambio di Icone. È JSON in chiaro, quindi si può scrivere a mano o generare da uno script senza aprire l'editor: `npx @icone/cli build progetto.icon` lo trasforma in componenti. Quasi tutto ha un valore predefinito — il documento più piccolo che funziona sta in venti righe.",

  "type": "object",
  "required": ["formato", "versione", "set"],
  "additionalProperties": false,
  "properties": {
    "formato": { "const": "icone" },
    "versione": { "type": "integer", "minimum": 1, "maximum": 1 },
    "salvatoIl": {
      "type": "string",
      "description": "ISO 8601. Indicativo: non viene usato per nulla se non mostrarlo."
    },
    "set": { "$ref": "#/$defs/set" }
  },

  "$defs": {
    "set": {
      "type": "object",
      "description": "Un insieme di icone che condividono griglia, stile e destinazione.",
      "required": ["id", "name", "icons"],
      "additionalProperties": false,
      "properties": {
        "id": { "type": "string" },
        "name": { "type": "string" },
        "size": { "$ref": "#/$defs/misura" },
        "grid": { "$ref": "#/$defs/griglia" },
        "tokens": { "type": "array", "items": { "$ref": "#/$defs/token" } },
        "icons": { "type": "array", "items": { "$ref": "#/$defs/icona" }, "minItems": 1 }
      }
    },

    "icona": {
      "type": "object",
      "required": ["id", "name", "states", "transitions"],
      "additionalProperties": false,
      "properties": {
        "id": { "type": "string" },
        "name": {
          "type": "string",
          "description": "Dà il nome al componente esportato: «cuore pieno» → `CuorePienoIcon`."
        },
        "modifiedAt": { "type": "number", "description": "Millisecondi dall'epoca." },
        "size": { "$ref": "#/$defs/misura" },
        "grid": { "$ref": "#/$defs/griglia" },
        "states": {
          "type": "array",
          "minItems": 1,
          "description": "I fotogrammi. Uno solo produce un'icona ferma.",
          "items": { "$ref": "#/$defs/stato" }
        },
        "transitions": {
          "type": "array",
          "description": "Archi di un grafo, non una catena. Vuoto se l'icona non si anima.",
          "items": { "$ref": "#/$defs/transizione" }
        },
        "tokens": { "type": "array", "items": { "$ref": "#/$defs/token" } }
      }
    },

    "stato": {
      "type": "object",
      "description": "Un fotogramma: la mappa piatta dei nodi, più la radice.",
      "required": ["id", "rootId", "nodes"],
      "additionalProperties": false,
      "properties": {
        "id": { "type": "string" },
        "name": { "type": "string" },
        "rootId": {
          "type": "string",
          "description": "Chiave, dentro `nodes`, del gruppo radice. Non viene disegnato."
        },
        "nodes": {
          "type": "object",
          "description": "Mappa piatta id → nodo, non un albero annidato. La gerarchia sta in `parentId`/`childIds`.",
          "additionalProperties": { "$ref": "#/$defs/nodo" }
        }
      }
    },

    "nodo": {
      "type": "object",
      "required": ["id", "geometry"],
      "additionalProperties": false,
      "properties": {
        "id": {
          "type": "string",
          "description": "Lo **stesso** id in due stati abbina i due nodi in modo esatto. È il modo di dire al motore che cosa diventa che cosa: usarlo evita del tutto l'euristica."
        },
        "name": { "type": "string", "default": "Livello" },
        "type": {
          "enum": ["group", "path", "rect", "ellipse", "line", "polygon"],
          "description": "Ridondante: se manca si ricava da `geometry.kind`."
        },
        "parentId": { "type": ["string", "null"], "default": null },
        "childIds": { "type": "array", "items": { "type": "string" }, "default": [] },
        "visible": { "type": "boolean", "default": true },
        "locked": { "type": "boolean", "default": false },
        "clipPathId": { "type": ["string", "null"], "default": null },
        "maskId": { "type": ["string", "null"], "default": null },
        "transform": { "$ref": "#/$defs/trasformazione" },
        "geometry": { "$ref": "#/$defs/geometria" },
        "style": { "$ref": "#/$defs/stile" }
      }
    },

    "geometria": {
      "description": "Le primitive restano primitive: un rettangolo scritto come `rect` resta modificabile come rettangolo, e in export diventa un `<rect>` invece che un tracciato.",
      "oneOf": [
        {
          "type": "object",
          "required": ["kind"],
          "additionalProperties": false,
          "properties": { "kind": { "const": "group" } }
        },
        {
          "type": "object",
          "required": ["kind", "commands"],
          "additionalProperties": false,
          "properties": {
            "kind": { "const": "path" },
            "commands": { "type": "array", "items": { "$ref": "#/$defs/comando" } }
          }
        },
        {
          "type": "object",
          "required": ["kind", "x", "y", "w", "h"],
          "additionalProperties": false,
          "properties": {
            "kind": { "const": "rect" },
            "x": { "type": "number" },
            "y": { "type": "number" },
            "w": { "type": "number" },
            "h": { "type": "number" },
            "radii": {
              "type": "array",
              "description": "Quattro raggi in senso orario da in alto a sinistra.",
              "items": { "type": "number", "minimum": 0 },
              "minItems": 4,
              "maxItems": 4
            }
          }
        },
        {
          "type": "object",
          "required": ["kind", "cx", "cy", "rx", "ry"],
          "additionalProperties": false,
          "properties": {
            "kind": { "const": "ellipse" },
            "cx": { "type": "number" },
            "cy": { "type": "number" },
            "rx": { "type": "number", "minimum": 0 },
            "ry": { "type": "number", "minimum": 0 }
          }
        },
        {
          "type": "object",
          "required": ["kind", "x1", "y1", "x2", "y2"],
          "additionalProperties": false,
          "properties": {
            "kind": { "const": "line" },
            "x1": { "type": "number" },
            "y1": { "type": "number" },
            "x2": { "type": "number" },
            "y2": { "type": "number" }
          }
        },
        {
          "type": "object",
          "required": ["kind", "points"],
          "additionalProperties": false,
          "properties": {
            "kind": { "const": "polygon" },
            "points": { "type": "array", "items": { "$ref": "#/$defs/punto" } },
            "closed": { "type": "boolean", "default": true }
          }
        }
      ]
    },

    "comando": {
      "description": "Solo M, L, C, Z. Archi e quadratiche vanno convertiti in cubiche prima: un solo caso da gestire rende possibili morphing, booleane e semplificazione.",
      "oneOf": [
        {
          "type": "object",
          "required": ["type", "x", "y"],
          "additionalProperties": false,
          "properties": {
            "type": { "enum": ["M", "L"] },
            "x": { "type": "number" },
            "y": { "type": "number" }
          }
        },
        {
          "type": "object",
          "required": ["type", "x1", "y1", "x2", "y2", "x", "y"],
          "additionalProperties": false,
          "properties": {
            "type": { "const": "C" },
            "x1": { "type": "number" },
            "y1": { "type": "number" },
            "x2": { "type": "number" },
            "y2": { "type": "number" },
            "x": { "type": "number" },
            "y": { "type": "number" }
          }
        },
        {
          "type": "object",
          "required": ["type"],
          "additionalProperties": false,
          "properties": { "type": { "const": "Z" } }
        }
      ]
    },

    "punto": {
      "type": "object",
      "required": ["x", "y"],
      "additionalProperties": false,
      "properties": { "x": { "type": "number" }, "y": { "type": "number" } }
    },

    "trasformazione": {
      "type": "object",
      "description": "Sempre decomposta, mai una matrice. Interpolare due matrici produce movimenti che si accartocciano; questa forma permette di ruotare per l'arco più breve.",
      "additionalProperties": false,
      "properties": {
        "x": { "type": "number", "default": 0 },
        "y": { "type": "number", "default": 0 },
        "rotation": { "type": "number", "default": 0, "description": "Gradi." },
        "scaleX": { "type": "number", "default": 1 },
        "scaleY": { "type": "number", "default": 1 },
        "skewX": { "type": "number", "default": 0, "description": "Gradi." },
        "originX": {
          "type": "number",
          "default": 0.5,
          "description": "Relativo al riquadro del nodo, 0..1. Con 0.5/0.5 «ruota sul proprio centro» resta vero anche se la geometria cambia."
        },
        "originY": { "type": "number", "default": 0.5 }
      }
    },

    "stile": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "fill": { "$ref": "#/$defs/paint" },
        "fillOpacity": { "type": "number", "minimum": 0, "maximum": 1, "default": 1 },
        "fillRule": { "enum": ["nonzero", "evenodd"], "default": "nonzero" },

        "stroke": { "$ref": "#/$defs/paint" },
        "strokeOpacity": { "type": "number", "minimum": 0, "maximum": 1, "default": 1 },
        "strokeWidth": { "type": "number", "minimum": 0, "default": 2 },
        "strokeLineCap": { "enum": ["butt", "round", "square"], "default": "round" },
        "strokeLineJoin": { "enum": ["miter", "round", "bevel"], "default": "round" },
        "strokeMiterLimit": { "type": "number", "minimum": 1, "default": 4 },
        "strokeDashArray": {
          "type": ["array", "null"],
          "items": { "type": "number", "minimum": 0 },
          "default": null
        },
        "strokeDashOffset": { "type": "number", "default": 0 },
        "strokeAlign": {
          "enum": ["center", "inside", "outside"],
          "default": "center",
          "description": "SVG non ha l'allineamento del tratto: `inside` e `outside` si realizzano in export con un offset path."
        },

        "opacity": { "type": "number", "minimum": 0, "maximum": 1, "default": 1 },
        "blendMode": {
          "enum": ["normal", "multiply", "screen", "overlay", "darken", "lighten", "color-dodge", "color-burn", "hard-light", "soft-light", "difference", "exclusion"],
          "default": "normal"
        },

        "trimStart": {
          "type": "number",
          "minimum": 0,
          "maximum": 1,
          "default": 0,
          "description": "Il trim è ciò che realizza il draw-on, cioè l'animazione delle icone a contorno: si anima `trimEnd` da 0 a 1 e il tratto si disegna."
        },
        "trimEnd": { "type": "number", "minimum": 0, "maximum": 1, "default": 1 },
        "trimOffset": { "type": "number", "minimum": -1, "maximum": 1, "default": 0 }
      }
    },

    "paint": {
      "oneOf": [
        {
          "type": "object",
          "required": ["kind"],
          "additionalProperties": false,
          "properties": { "kind": { "const": "none" } }
        },
        {
          "type": "object",
          "required": ["kind"],
          "additionalProperties": false,
          "description": "Eredita il colore dal CSS circostante. È il valore giusto per quasi ogni icona.",
          "properties": { "kind": { "const": "currentColor" } }
        },
        {
          "type": "object",
          "required": ["kind", "color"],
          "additionalProperties": false,
          "properties": {
            "kind": { "const": "solid" },
            "color": {
              "type": "string",
              "description": "Esadecimale, nome CSS, `rgb()`, `hsl()`, `oklch()`, `oklab()`, `lab()`, `hwb()`."
            }
          }
        },
        {
          "type": "object",
          "required": ["kind", "tokenId"],
          "additionalProperties": false,
          "properties": { "kind": { "const": "token" }, "tokenId": { "type": "string" } }
        },
        {
          "type": "object",
          "required": ["kind", "gradient"],
          "additionalProperties": false,
          "properties": { "kind": { "const": "gradient" }, "gradient": { "$ref": "#/$defs/gradiente" } }
        }
      ]
    },

    "gradiente": {
      "oneOf": [
        {
          "type": "object",
          "required": ["type", "x1", "y1", "x2", "y2", "stops"],
          "additionalProperties": false,
          "properties": {
            "type": { "const": "linear" },
            "x1": { "type": "number" },
            "y1": { "type": "number" },
            "x2": { "type": "number" },
            "y2": { "type": "number" },
            "stops": { "type": "array", "items": { "$ref": "#/$defs/fermata" }, "minItems": 1 }
          }
        },
        {
          "type": "object",
          "required": ["type", "cx", "cy", "r", "stops"],
          "additionalProperties": false,
          "properties": {
            "type": { "const": "radial" },
            "cx": { "type": "number" },
            "cy": { "type": "number" },
            "r": { "type": "number", "minimum": 0 },
            "stops": { "type": "array", "items": { "$ref": "#/$defs/fermata" }, "minItems": 1 }
          }
        }
      ]
    },

    "fermata": {
      "type": "object",
      "required": ["offset", "color"],
      "additionalProperties": false,
      "properties": {
        "offset": { "type": "number", "minimum": 0, "maximum": 1 },
        "color": { "type": "string" },
        "opacity": { "type": "number", "minimum": 0, "maximum": 1, "default": 1 }
      }
    },

    "token": {
      "type": "object",
      "description": "Un colore con un nome, riferito dalle forme con `{ \"kind\": \"token\", \"tokenId\": \"…\" }`.",
      "required": ["id", "name", "color"],
      "additionalProperties": false,
      "properties": {
        "id": { "type": "string" },
        "name": { "type": "string" },
        "color": { "type": "string" }
      }
    },

    "transizione": {
      "type": "object",
      "description": "Come si passa da un fotogramma all'altro. `smart` abbina i livelli e interpola ciò che cambia; è l'unico valore che serve quasi sempre.",
      "required": ["id", "fromStateId", "toStateId"],
      "additionalProperties": false,
      "properties": {
        "id": { "type": "string" },
        "kind": { "enum": ["smart", "dissolve", "instant"], "default": "smart" },
        "fromStateId": { "type": "string" },
        "toStateId": { "type": "string" },
        "timing": { "$ref": "#/$defs/tempi" },
        "stagger": { "$ref": "#/$defs/sfalsamento" },
        "enter": { "$ref": "#/$defs/comparsa", "default": "fade" },
        "exit": { "$ref": "#/$defs/comparsa", "default": "fade" },
        "forcedPairs": {
          "type": "array",
          "description": "Abbinamenti corretti a mano. Servono solo quando i due stati usano id diversi: con gli stessi id l'abbinamento è già esatto.",
          "items": {
            "type": "object",
            "required": ["fromId", "toId"],
            "additionalProperties": false,
            "properties": { "fromId": { "type": "string" }, "toId": { "type": "string" } }
          }
        },
        "excludedIds": { "type": "array", "items": { "type": "string" } },
        "nodeOverrides": {
          "type": "object",
          "description": "Tempi diversi per un singolo livello.",
          "additionalProperties": { "$ref": "#/$defs/tempi" }
        },
        "nodeMotion": {
          "type": "object",
          "additionalProperties": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "spin": {
                "type": "number",
                "description": "Giri interi aggiuntivi. Senza, l'interpolazione sceglie sempre l'arco più breve e uno spinner non farebbe mai un giro completo."
              },
              "arc": { "type": "number", "description": "Curvatura della traiettoria." },
              "squash": {
                "type": "number",
                "minimum": 0,
                "maximum": 1,
                "description": "Squash & stretch: la forma si allunga dove va e si schiaccia quando rallenta, a volume costante. Zero la lascia rigida."
              }
            }
          }
        },
        "loop": { "$ref": "#/$defs/ciclo" },
        "trigger": {
          "type": "object",
          "required": ["kind"],
          "additionalProperties": false,
          "properties": {
            "kind": {
              "enum": ["none", "hover", "click", "toggle", "mount", "inView"],
              "default": "none",
              "description": "`none` significa in ciclo continuo. Gli altri producono un componente che reagisce a un evento."
            }
          }
        }
      }
    },

    "tempi": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "duration": { "type": "number", "minimum": 0, "maximum": 60000, "default": 300, "description": "Millisecondi." },
        "delay": { "type": "number", "minimum": 0, "maximum": 60000, "default": 0 },
        "easing": { "$ref": "#/$defs/andatura" }
      }
    },

    "andatura": {
      "oneOf": [
        {
          "type": "object",
          "required": ["kind"],
          "additionalProperties": false,
          "properties": { "kind": { "const": "linear" } }
        },
        {
          "type": "object",
          "required": ["kind", "p1x", "p1y", "p2x", "p2y"],
          "additionalProperties": false,
          "properties": {
            "kind": { "const": "cubic" },
            "p1x": { "type": "number", "minimum": 0, "maximum": 1 },
            "p1y": { "type": "number" },
            "p2x": { "type": "number", "minimum": 0, "maximum": 1 },
            "p2y": { "type": "number" }
          }
        },
        {
          "type": "object",
          "required": ["kind", "mass", "stiffness", "damping"],
          "additionalProperties": false,
          "description": "La molla ricava la propria durata dai parametri: `duration` viene ignorato.",
          "properties": {
            "kind": { "const": "spring" },
            "mass": { "type": "number", "exclusiveMinimum": 0 },
            "stiffness": { "type": "number", "exclusiveMinimum": 0 },
            "damping": { "type": "number", "minimum": 0 }
          }
        },
        {
          "type": "object",
          "required": ["kind", "bounces"],
          "additionalProperties": false,
          "properties": { "kind": { "const": "bounce" }, "bounces": { "type": "number", "minimum": 0 } }
        },
        {
          "type": "object",
          "required": ["kind", "amplitude", "period"],
          "additionalProperties": false,
          "properties": {
            "kind": { "const": "elastic" },
            "amplitude": { "type": "number" },
            "period": { "type": "number", "exclusiveMinimum": 0 }
          }
        },
        {
          "type": "object",
          "required": ["kind", "count"],
          "additionalProperties": false,
          "properties": { "kind": { "const": "steps" }, "count": { "type": "number", "minimum": 1 } }
        }
      ]
    },

    "sfalsamento": {
      "type": "object",
      "description": "Fa partire i livelli uno dopo l'altro invece che tutti insieme.",
      "additionalProperties": false,
      "properties": {
        "mode": {
          "enum": ["none", "order", "positionX", "positionY", "distanceFromCenter"],
          "default": "none"
        },
        "amount": { "type": "number", "minimum": 0, "maximum": 60000, "default": 40, "description": "Millisecondi fra un livello e il successivo." },
        "reverse": { "type": "boolean", "default": false }
      }
    },

    "ciclo": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "mode": { "enum": ["once", "loop", "pingPong"], "default": "once" },
        "count": { "type": "integer", "minimum": 0, "default": 1, "description": "Zero significa senza fine." },
        "hold": { "type": "number", "minimum": 0, "maximum": 60000, "default": 0, "description": "Pausa fra una ripetizione e la successiva, in millisecondi." }
      }
    },

    "comparsa": {
      "enum": ["none", "fade", "scale", "slideUp", "slideDown", "drawOn", "wipeRight", "wipeUp"],
      "description": "Che cosa fa un livello presente in un solo fotogramma. `drawOn` disegna il tratto invece di farlo comparire; `wipeRight` e `wipeUp` scoprono la forma a tendina, da sinistra o dal basso."
    },

    "misura": {
      "type": "object",
      "required": ["w", "h"],
      "additionalProperties": false,
      "description": "Il riquadro del documento. 24×24 è la convenzione più diffusa.",
      "properties": {
        "w": { "type": "number", "exclusiveMinimum": 0, "maximum": 1024 },
        "h": { "type": "number", "exclusiveMinimum": 0, "maximum": 1024 }
      }
    },

    "griglia": {
      "type": "object",
      "description": "Solo un aiuto al disegno: non tocca la geometria e non compare nell'export.",
      "additionalProperties": false,
      "properties": {
        "step": { "type": "number", "exclusiveMinimum": 0, "default": 1 },
        "safeArea": { "type": "number", "minimum": 0, "default": 2 },
        "showGrid": { "type": "boolean", "default": true },
        "showKeylines": { "type": "boolean", "default": false },
        "snap": { "type": "boolean", "default": true }
      }
    }
  }
}
