{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://opencoachingformat.org/schema/v1.json",
  "$comment": "Schema version 1.4.0. The $id stays v1 across all backwards-compatible 1.x releases; version-pinned copies live at /<version>/ocf-action-v1.json.",
  "x-ocf-version": "1.4.0",
  "title": "Open Coaching Format",
  "description": "Open standard for team-sport coaching diagrams and animations (invasion team sports; basketball first). Semantic action model.",
  "type": "object",
  "required": ["meta", "court", "entities", "frames"],

  "definitions": {

    "ruleset": {
      "type": "string",
      "enum": ["fiba", "nba", "ncaa", "nfhs", "custom"],
      "description": "Basketball ruleset. Determines unit, field dimensions and named position coordinates."
    },

    "unit": {
      "type": "string",
      "enum": ["m", "ft"],
      "description": "Length unit used for all coordinates in this document. Derived from ruleset unless custom."
    },

    "coordinate_free": {
      "type": "object",
      "description": "Absolute coordinate in court units. Origin at center of court. x: negative=viewer's left, positive=viewer's right. y: positive=frontcourt (offense basket), negative=backcourt.",
      "required": ["x", "y"],
      "properties": { "x": { "type": "number" }, "y": { "type": "number" } },
      "additionalProperties": false
    },

    "coordinate_named": {
      "type": "object",
      "description": "Reference to a named court position resolved via the active ruleset. Custom positions use 'custom.' prefix.",
      "required": ["named"],
      "properties": { "named": { "type": "string" } },
      "additionalProperties": false
    },

    "coordinate_relative": {
      "type": "object",
      "description": "Offset from a named court position, in court units.",
      "required": ["relative_to", "dx", "dy"],
      "properties": {
        "relative_to": { "type": "string" },
        "dx": { "type": "number" },
        "dy": { "type": "number" }
      },
      "additionalProperties": false
    },

    "coordinate": {
      "description": "A single point: absolute, named, or relative.",
      "oneOf": [
        { "$ref": "#/definitions/coordinate_free" },
        { "$ref": "#/definitions/coordinate_named" },
        { "$ref": "#/definitions/coordinate_relative" }
      ]
    },

    "entity_ref": {
      "type": "string",
      "description": "Reference to a player, coach, cone or station. Format '{offense|defense}_{1-9}', 'coach', 'cone_N', 'station_N'.",
      "pattern": "^(offense|defense)_[1-9]$|^coach$|^(cone|station)_[1-9][0-9]*$"
    },

    "ball_ref": {
      "type": "string",
      "description": "Reference to a ball by id, e.g. 'ball_1'.",
      "pattern": "^ball_[1-9][0-9]*$"
    },

    "color_role": {
      "type": "string",
      "enum": ["offense", "defense", "black", "grey", "yellow", "green", "red", "blue", "white"],
      "description": "Semantic color role. Resolved to hex via color_scheme."
    },

    "entity_offense": {
      "type": "object",
      "description": "Offensive player.",
      "required": ["type", "nr", "x", "y"],
      "properties": {
        "type": { "type": "string", "const": "offense" },
        "nr": { "type": "integer", "minimum": 1, "maximum": 9 },
        "x": { "type": "number" },
        "y": { "type": "number" },
        "rotation": { "type": "number", "minimum": 0, "maximum": 360, "default": 0 },
        "color": { "$ref": "#/definitions/color_role", "default": "offense" },
        "label": { "type": "string" }
      },
      "additionalProperties": false
    },

    "entity_defense": {
      "type": "object",
      "description": "Defensive player.",
      "required": ["type", "nr", "x", "y"],
      "properties": {
        "type": { "type": "string", "const": "defense" },
        "nr": { "type": "integer", "minimum": 1, "maximum": 9 },
        "x": { "type": "number" },
        "y": { "type": "number" },
        "rotation": { "type": "number", "minimum": 0, "maximum": 360, "default": 0 },
        "color": { "$ref": "#/definitions/color_role", "default": "defense" },
        "label": { "type": "string" }
      },
      "additionalProperties": false
    },

    "entity_coach": {
      "type": "object",
      "description": "Coach position marker. May carry a ball.",
      "required": ["type", "x", "y"],
      "properties": {
        "type": { "type": "string", "const": "coach" },
        "x": { "type": "number" },
        "y": { "type": "number" }
      },
      "additionalProperties": false
    },

    "entity_cone": {
      "type": "object",
      "description": "Training cone / marker.",
      "required": ["type", "nr", "x", "y"],
      "properties": {
        "type": { "type": "string", "const": "cone" },
        "nr": { "type": "integer", "minimum": 1 },
        "x": { "type": "number" },
        "y": { "type": "number" }
      },
      "additionalProperties": false
    },

    "entity_station": {
      "type": "object",
      "description": "Numbered station for circuit training.",
      "required": ["type", "nr", "x", "y"],
      "properties": {
        "type": { "type": "string", "const": "station" },
        "nr": { "type": "integer", "minimum": 1 },
        "label": { "type": "string" },
        "x": { "type": "number" },
        "y": { "type": "number" }
      },
      "additionalProperties": false
    },

    "entity": {
      "description": "Any non-ball entity on the court.",
      "oneOf": [
        { "$ref": "#/definitions/entity_offense" },
        { "$ref": "#/definitions/entity_defense" },
        { "$ref": "#/definitions/entity_coach" },
        { "$ref": "#/definitions/entity_cone" },
        { "$ref": "#/definitions/entity_station" }
      ]
    },

    "ball": {
      "type": "object",
      "description": "A ball. Holds exactly one lifecycle state: carried_by, at, or dead.",
      "required": ["id"],
      "properties": {
        "id": { "$ref": "#/definitions/ball_ref" },
        "carried_by": { "$ref": "#/definitions/entity_ref" },
        "at": { "$ref": "#/definitions/coordinate" },
        "dead": { "type": "boolean", "const": true }
      },
      "additionalProperties": false,
      "oneOf": [
        { "required": ["carried_by"], "not": { "anyOf": [ { "required": ["at"] }, { "required": ["dead"] } ] } },
        { "required": ["at"], "not": { "anyOf": [ { "required": ["carried_by"] }, { "required": ["dead"] } ] } },
        { "required": ["dead"], "not": { "anyOf": [ { "required": ["carried_by"] }, { "required": ["at"] } ] } }
      ]
    },

    "ball_state": {
      "type": "object",
      "description": "A ball's lifecycle state inside a frame state, keyed by ball ref. Exactly one of carried_by/at/dead.",
      "properties": {
        "carried_by": { "$ref": "#/definitions/entity_ref" },
        "at": { "$ref": "#/definitions/coordinate" },
        "dead": { "type": "boolean", "const": true }
      },
      "additionalProperties": false,
      "oneOf": [
        { "required": ["carried_by"], "not": { "anyOf": [ { "required": ["at"] }, { "required": ["dead"] } ] } },
        { "required": ["at"], "not": { "anyOf": [ { "required": ["carried_by"] }, { "required": ["dead"] } ] } },
        { "required": ["dead"], "not": { "anyOf": [ { "required": ["carried_by"] }, { "required": ["at"] } ] } }
      ]
    },

    "state": {
      "type": "object",
      "description": "Positional + ball anchor. Entity refs map to a coordinate; the optional 'balls' map keys ball refs to ball states.",
      "properties": {
        "balls": {
          "type": "object",
          "propertyNames": { "$ref": "#/definitions/ball_ref" },
          "additionalProperties": { "$ref": "#/definitions/ball_state" }
        }
      },
      "patternProperties": {
        "^(offense|defense)_[1-9]$|^coach$|^(cone|station)_[1-9][0-9]*$": { "$ref": "#/definitions/coordinate" }
      },
      "additionalProperties": false
    },

    "action_ref": {
      "type": "string",
      "description": "Reference to another action in the same frame: '<entity_ref>.<action_type>'.",
      "pattern": "^((offense|defense)_[1-9]|coach|(cone|station)_[1-9][0-9]*)\\.(move|cut|screen|defend|dribble|pass|shoot|rebound|pickup|tackle|clear|faceoff|check)$"
    },

    "movement_intensity": {
      "type": "string",
      "description": "Relative tempo over distance for move/cut/dribble. Does not imply a concrete duration — a downstream application derives timing from this plus actual player speed.",
      "enum": ["slow", "normal", "fast", "explosive"]
    },

    "ball_intensity": {
      "type": "string",
      "description": "Relative ball speed for pass/shoot. Independent of the pass 'variant' (technique/flight path), e.g. variant 'lob' + intensity 'soft' are separate axes.",
      "enum": ["soft", "normal", "hard", "bullet"]
    },

    "physicality": {
      "type": "string",
      "description": "Contact/collision style for screen/defend/rebound/pickup, used by a renderer to pick the right animation. Independent of intensity — no effect on distance/timing.",
      "enum": ["passive", "normal", "aggressive", "hard"]
    },

    "move_step": {
      "type": "object",
      "description": "One step in a movement sequence. Without 'to' = a move on the spot. Reference fields override the action-level ones.",
      "properties": {
        "variant": { "type": "string" },
        "to": { "$ref": "#/definitions/coordinate" },
        "around_player": { "$ref": "#/definitions/entity_ref" },
        "off_screen_by": { "$ref": "#/definitions/entity_ref" },
        "intensity": { "$ref": "#/definitions/movement_intensity" },
        "side": { "type": "string", "enum": ["left", "right"], "description": "Which side to pass an around_player obstacle on, relative to the moving player's direction of travel. Optional; renderer chooses when absent." },
        "arc": { "type": "string", "enum": ["tight", "normal", "wide"], "description": "How closely the path wraps an around_player obstacle: tight (curl), normal, wide (flare). Optional; renderer default when absent." }
      },
      "additionalProperties": false
    },

    "action_move": {
      "type": "object",
      "required": ["player", "type", "moves"],
      "properties": {
        "player": { "$ref": "#/definitions/entity_ref" },
        "type": { "const": "move" },
        "moves": { "type": "array", "minItems": 1, "items": { "$ref": "#/definitions/move_step" } },
        "intensity": { "$ref": "#/definitions/movement_intensity" },
        "tags": { "type": "array", "items": { "type": "string" } },
        "after": { "$ref": "#/definitions/action_ref" },
        "with": { "$ref": "#/definitions/action_ref" },
        "on_catch": { "type": "boolean" }
      },
      "additionalProperties": false
    },

    "action_cut": {
      "type": "object",
      "required": ["player", "type", "moves"],
      "properties": {
        "player": { "$ref": "#/definitions/entity_ref" },
        "type": { "const": "cut" },
        "moves": { "type": "array", "minItems": 1, "items": { "$ref": "#/definitions/move_step" } },
        "variant": { "type": "string", "enum": ["backdoor", "give_and_go", "flash", "v_cut", "l_cut", "curl", "flare", "fade", "basket"] },
        "around_player": { "$ref": "#/definitions/entity_ref" },
        "off_screen_by": { "$ref": "#/definitions/entity_ref" },
        "side": { "type": "string", "enum": ["left", "right"], "description": "Default side for this cut's around_player steps; overridden per move_step." },
        "arc": { "type": "string", "enum": ["tight", "normal", "wide"], "description": "Default arc for this cut's around_player steps; overridden per move_step." },
        "intensity": { "$ref": "#/definitions/movement_intensity" },
        "tags": { "type": "array", "items": { "type": "string" } },
        "after": { "$ref": "#/definitions/action_ref" },
        "with": { "$ref": "#/definitions/action_ref" },
        "on_catch": { "type": "boolean" }
      },
      "additionalProperties": false
    },

    "action_screen": {
      "type": "object",
      "required": ["player", "type", "for_player"],
      "properties": {
        "player": { "$ref": "#/definitions/entity_ref" },
        "type": { "const": "screen" },
        "for_player": { "$ref": "#/definitions/entity_ref" },
        "on_player": { "$ref": "#/definitions/entity_ref" },
        "at": { "$ref": "#/definitions/coordinate" },
        "variant": { "type": "string", "enum": ["ball_screen", "back_screen", "down_screen", "flare_screen", "cross_screen", "pin_down"] },
        "physicality": { "$ref": "#/definitions/physicality" },
        "tags": { "type": "array", "items": { "type": "string" } },
        "after": { "$ref": "#/definitions/action_ref" },
        "with": { "$ref": "#/definitions/action_ref" },
        "on_catch": { "type": "boolean" }
      },
      "additionalProperties": false
    },

    "action_defend": {
      "type": "object",
      "required": ["player", "type", "guards_player"],
      "properties": {
        "player": { "$ref": "#/definitions/entity_ref" },
        "type": { "const": "defend" },
        "guards_player": { "$ref": "#/definitions/entity_ref" },
        "variant": { "type": "string", "enum": ["on_ball", "deny", "help", "hedge", "switch", "box_out"] },
        "physicality": { "$ref": "#/definitions/physicality" },
        "tags": { "type": "array", "items": { "type": "string" } },
        "after": { "$ref": "#/definitions/action_ref" },
        "with": { "$ref": "#/definitions/action_ref" },
        "on_catch": { "type": "boolean" }
      },
      "additionalProperties": false
    },

    "action_dribble": {
      "type": "object",
      "required": ["player", "type", "moves"],
      "properties": {
        "player": { "$ref": "#/definitions/entity_ref" },
        "type": { "const": "dribble" },
        "moves": { "type": "array", "minItems": 1, "items": { "$ref": "#/definitions/move_step" } },
        "ball_id": { "$ref": "#/definitions/ball_ref" },
        "intensity": { "$ref": "#/definitions/movement_intensity" },
        "tags": { "type": "array", "items": { "type": "string" } },
        "after": { "$ref": "#/definitions/action_ref" },
        "with": { "$ref": "#/definitions/action_ref" },
        "on_catch": { "type": "boolean" }
      },
      "additionalProperties": false
    },

    "action_pass": {
      "type": "object",
      "required": ["player", "type", "to_player"],
      "properties": {
        "player": { "$ref": "#/definitions/entity_ref" },
        "type": { "const": "pass" },
        "to_player": { "$ref": "#/definitions/entity_ref" },
        "ball_id": { "$ref": "#/definitions/ball_ref" },
        "variant": { "type": "string", "enum": ["chest", "bounce", "overhead", "lob", "baseball", "hand_off", "outlet"] },
        "intensity": { "$ref": "#/definitions/ball_intensity" },
        "tags": { "type": "array", "items": { "type": "string" } },
        "after": { "$ref": "#/definitions/action_ref" },
        "with": { "$ref": "#/definitions/action_ref" },
        "on_catch": { "type": "boolean" }
      },
      "additionalProperties": false
    },

    "action_shoot": {
      "type": "object",
      "required": ["player", "type"],
      "properties": {
        "player": { "$ref": "#/definitions/entity_ref" },
        "type": { "const": "shoot" },
        "ball_id": { "$ref": "#/definitions/ball_ref" },
        "variant": { "type": "string", "enum": ["jumper", "three", "layup", "floater", "dunk", "hook", "free_throw"] },
        "result": { "type": "string", "enum": ["make", "miss"] },
        "intensity": { "$ref": "#/definitions/ball_intensity" },
        "tags": { "type": "array", "items": { "type": "string" } },
        "after": { "$ref": "#/definitions/action_ref" },
        "with": { "$ref": "#/definitions/action_ref" },
        "on_catch": { "type": "boolean" }
      },
      "additionalProperties": false
    },

    "action_rebound": {
      "type": "object",
      "required": ["player", "type"],
      "properties": {
        "player": { "$ref": "#/definitions/entity_ref" },
        "type": { "const": "rebound" },
        "ball_id": { "$ref": "#/definitions/ball_ref" },
        "variant": { "type": "string", "enum": ["offensive", "defensive"] },
        "physicality": { "$ref": "#/definitions/physicality" },
        "tags": { "type": "array", "items": { "type": "string" } },
        "after": { "$ref": "#/definitions/action_ref" },
        "with": { "$ref": "#/definitions/action_ref" },
        "on_catch": { "type": "boolean" }
      },
      "additionalProperties": false
    },

    "action_pickup": {
      "type": "object",
      "required": ["player", "type", "ball_id"],
      "properties": {
        "player": { "$ref": "#/definitions/entity_ref" },
        "type": { "const": "pickup" },
        "ball_id": { "$ref": "#/definitions/ball_ref" },
        "physicality": { "$ref": "#/definitions/physicality" },
        "tags": { "type": "array", "items": { "type": "string" } },
        "after": { "$ref": "#/definitions/action_ref" },
        "with": { "$ref": "#/definitions/action_ref" },
        "on_catch": { "type": "boolean" }
      },
      "additionalProperties": false
    },

    "action_tackle": {
      "type": "object",
      "$comment": "Reserved for invasion sports (soccer/futsal/hockey). No variants defined yet; must be specified before promotion.",
      "required": ["player", "type"],
      "properties": {
        "player": { "$ref": "#/definitions/entity_ref" },
        "type": { "const": "tackle" },
        "physicality": { "$ref": "#/definitions/physicality" },
        "tags": { "type": "array", "items": { "type": "string" } },
        "after": { "$ref": "#/definitions/action_ref" },
        "with": { "$ref": "#/definitions/action_ref" },
        "on_catch": { "type": "boolean" }
      },
      "additionalProperties": false
    },

    "action_clear": {
      "type": "object",
      "$comment": "Reserved for invasion sports (soccer/futsal/hockey). No variants defined yet.",
      "required": ["player", "type"],
      "properties": {
        "player": { "$ref": "#/definitions/entity_ref" },
        "type": { "const": "clear" },
        "ball_id": { "$ref": "#/definitions/ball_ref" },
        "intensity": { "$ref": "#/definitions/ball_intensity" },
        "tags": { "type": "array", "items": { "type": "string" } },
        "after": { "$ref": "#/definitions/action_ref" },
        "with": { "$ref": "#/definitions/action_ref" },
        "on_catch": { "type": "boolean" }
      },
      "additionalProperties": false
    },

    "action_faceoff": {
      "type": "object",
      "$comment": "Reserved for hockey. No variants defined yet.",
      "required": ["player", "type"],
      "properties": {
        "player": { "$ref": "#/definitions/entity_ref" },
        "type": { "const": "faceoff" },
        "tags": { "type": "array", "items": { "type": "string" } },
        "after": { "$ref": "#/definitions/action_ref" },
        "with": { "$ref": "#/definitions/action_ref" },
        "on_catch": { "type": "boolean" }
      },
      "additionalProperties": false
    },

    "action_check": {
      "type": "object",
      "$comment": "Reserved for hockey. No variants defined yet.",
      "required": ["player", "type"],
      "properties": {
        "player": { "$ref": "#/definitions/entity_ref" },
        "type": { "const": "check" },
        "on_player": { "$ref": "#/definitions/entity_ref" },
        "physicality": { "$ref": "#/definitions/physicality" },
        "tags": { "type": "array", "items": { "type": "string" } },
        "after": { "$ref": "#/definitions/action_ref" },
        "with": { "$ref": "#/definitions/action_ref" },
        "on_catch": { "type": "boolean" }
      },
      "additionalProperties": false
    },

    "action": {
      "description": "Any action. Discriminated by 'type'.",
      "oneOf": [
        { "$ref": "#/definitions/action_move" },
        { "$ref": "#/definitions/action_cut" },
        { "$ref": "#/definitions/action_screen" },
        { "$ref": "#/definitions/action_defend" },
        { "$ref": "#/definitions/action_dribble" },
        { "$ref": "#/definitions/action_pass" },
        { "$ref": "#/definitions/action_shoot" },
        { "$ref": "#/definitions/action_rebound" },
        { "$ref": "#/definitions/action_pickup" },
        { "$ref": "#/definitions/action_tackle" },
        { "$ref": "#/definitions/action_clear" },
        { "$ref": "#/definitions/action_faceoff" },
        { "$ref": "#/definitions/action_check" }
      ]
    },

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

    "formation_adjustment": {
      "type": "object",
      "description": "Per-entity delta from the resolved formation position. dx/dy are in court units, anchored to that entity's registry position (not a named position, so no relative_to).",
      "required": ["entity", "dx", "dy"],
      "properties": {
        "entity": { "$ref": "#/definitions/entity_ref" },
        "dx": { "type": "number" },
        "dy": { "type": "number" },
        "note": { "type": "string" }
      },
      "additionalProperties": false
    },

    "based_on_formation": {
      "type": "object",
      "description": "External reference to a starting formation in a versioned registry, resolved to entities at authoring time; pure provenance thereafter. Resolve by id, never by title.",
      "required": ["id"],
      "properties": {
        "id": { "type": "string", "description": "Stable formation id in the registry. Only field with normative meaning." },
        "title": { "type": "string", "description": "Human-readable label, convenience only." },
        "source": { "type": "string", "format": "uri", "description": "Stable URI of the formation registry index. Any URI; not host-restricted." },
        "source_version": {
          "type": "string",
          "description": "SemVer of the referenced registry. Optional; when present MUST be valid SemVer.",
          "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$"
        },
        "adjustments": { "type": "array", "items": { "$ref": "#/definitions/formation_adjustment" } }
      },
      "additionalProperties": false
    },

    "based_on_play": {
      "type": "object",
      "description": "External reference to another play this one derives from. The referenced play is always the base; this document is the derivative. Resolve by id, never by title.",
      "required": ["id"],
      "properties": {
        "id": { "type": "string", "description": "Stable play id in the referenced playbook index. Only field with normative meaning." },
        "title": { "type": "string", "description": "Human-readable label, convenience only." },
        "source": { "type": "string", "format": "uri", "description": "Stable URI of the playbook index/manifest. Any URI; not host-restricted." },
        "source_version": {
          "type": "string",
          "description": "SemVer of the referenced playbook. Optional; when present MUST be valid SemVer.",
          "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$"
        },
        "relationship": {
          "type": "string",
          "enum": ["variant", "progression", "counter"],
          "description": "How this play relates to the base. Direction is fixed: this play derives from the referenced base."
        }
      },
      "additionalProperties": false
    },

    "color_scheme": {
      "type": "object",
      "description": "Hex color values for each semantic role.",
      "properties": {
        "offense_fill": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$", "default": "#003366" },
        "offense_stroke": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$", "default": "#ffffff" },
        "defense_fill": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$", "default": "#58001d" },
        "defense_stroke": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$", "default": "#ffffff" },
        "black": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$", "default": "#000000" },
        "grey": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$", "default": "#7f7f7f" },
        "yellow": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$", "default": "#ffff00" },
        "green": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$", "default": "#7ce86a" },
        "red": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$", "default": "#ff0000" },
        "blue": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$", "default": "#5dd5ff" },
        "white": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$", "default": "#ffffff" }
      },
      "additionalProperties": false
    },

    "area": {
      "type": "object",
      "required": ["form", "x", "y"],
      "properties": {
        "form": { "type": "string", "enum": ["rectangle", "ellipse", "triangle"] },
        "color": { "$ref": "#/definitions/color_role", "default": "yellow" },
        "opacity": { "type": "number", "minimum": 0, "maximum": 1, "default": 0.4 },
        "x": { "type": "number" },
        "y": { "type": "number" },
        "width": { "type": "number" },
        "height": { "type": "number" },
        "rotation": { "type": "number", "minimum": 0, "maximum": 360, "default": 0 },
        "coords": { "type": "array", "minItems": 3, "maxItems": 3, "items": { "$ref": "#/definitions/coordinate" } }
      },
      "additionalProperties": false
    },

    "label": {
      "type": "object",
      "required": ["text", "x", "y"],
      "properties": {
        "text": { "type": "string" },
        "x": { "type": "number" },
        "y": { "type": "number" },
        "color": { "$ref": "#/definitions/color_role", "default": "black" }
      },
      "additionalProperties": false
    },

    "outcome": {
      "type": "string",
      "enum": ["make", "miss", "turnover", "steal", "foul"],
      "description": "Result that selects a branch target frame."
    },

    "frame": {
      "type": "object",
      "description": "A coaching phase: explicit anchors (start_state/end_state) plus semantic actions, with optional outcome branches.",
      "required": ["id", "actions", "end_state"],
      "properties": {
        "id": { "type": "string", "description": "Unique frame id within the drill." },
        "label": { "type": "string" },
        "description": { "type": "string", "description": "Coaching instruction; LLMs should generate this." },
        "duration_ms": { "type": "integer", "minimum": 0, "description": "Deprecated: use per-action 'intensity' instead. Suggested animation duration for the phase." },
        "start_state": {
          "$ref": "#/definitions/state",
          "description": "Optional. Defaults to the previous frame's end_state."
        },
        "actions": {
          "type": "array",
          "description": "Semantic actions in this phase. May be empty.",
          "items": { "$ref": "#/definitions/action" }
        },
        "end_state": {
          "$ref": "#/definitions/state",
          "description": "Explicit anchor at the end of the phase."
        },
        "branches": {
          "type": "object",
          "description": "Optional outcome -> target frame id. Without branches, the next frame in the array follows.",
          "propertyNames": { "$ref": "#/definitions/outcome" },
          "additionalProperties": { "type": "string" },
          "minProperties": 1
        }
      },
      "additionalProperties": false
    }
  },

  "properties": {

    "$schema": { "type": "string", "format": "uri" },

    "sport": {
      "type": "string",
      "enum": ["basketball", "soccer", "handball", "hockey", "futsal"],
      "default": "basketball",
      "$comment": "Optional, default basketball (back-compat). Intended to become required in v2.0.0. basketball is fully defined; soccer/handball/hockey/futsal carry provisional minimal action whitelists pending sport-expert review."
    },

    "meta": {
      "type": "object",
      "required": ["id", "title"],
      "properties": {
        "id": { "type": "string", "format": "uuid" },
        "title": { "type": "string" },
        "description": { "type": "string" },
        "author": { "type": "string" },
        "tags": { "type": "array", "items": { "type": "string" } },
        "difficulty": { "type": "string", "enum": ["beginner", "intermediate", "advanced"] },
        "created": { "type": "string", "format": "date-time" },
        "modified": { "type": "string", "format": "date-time" },
        "source_format": { "type": "string", "enum": ["fiba", "fastdraw", "open", "custom"] },
        "source_url": { "type": "string", "format": "uri" },
        "based_on_formation": { "$ref": "#/definitions/based_on_formation" },
        "based_on_play": { "$ref": "#/definitions/based_on_play" },
        "min_schema_version": {
          "type": "string",
          "description": "Minimum OCF schema version required to validate this document correctly. Optional; when present MUST be valid SemVer. A validator whose bundled schema is older SHOULD warn that it may be out of date rather than treat unknown newer fields as errors.",
          "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$"
        }
      },
      "additionalProperties": false
    },

    "court": {
      "type": "object",
      "required": ["ruleset", "type"],
      "properties": {
        "ruleset": { "$ref": "#/definitions/ruleset" },
        "type": { "type": "string", "enum": ["half_court", "full_court"] },
        "drill_focus": { "type": "string", "enum": ["offense", "defense", "transition", "neutral"], "default": "offense" },
        "wheelchair": { "type": "boolean", "default": false },
        "custom_dimensions": {
          "type": "object",
          "required": ["unit", "length", "width", "basket_from_baseline", "three_point_distance", "paint_width", "paint_depth", "free_throw_distance"],
          "properties": {
            "unit": { "$ref": "#/definitions/unit" },
            "length": { "type": "number" },
            "width": { "type": "number" },
            "basket_from_baseline": { "type": "number" },
            "three_point_distance": { "type": "number" },
            "paint_width": { "type": "number" },
            "paint_depth": { "type": "number" },
            "free_throw_distance": { "type": "number" }
          },
          "additionalProperties": false
        }
      },
      "additionalProperties": false
    },

    "color_scheme": { "$ref": "#/definitions/color_scheme" },

    "named_positions": {
      "type": "object",
      "properties": {
        "custom": {
          "type": "object",
          "additionalProperties": { "$ref": "#/definitions/custom_position" }
        }
      },
      "additionalProperties": false
    },

    "entities": {
      "type": "array",
      "description": "All non-ball entities and their initial (frame 0) positions.",
      "items": { "$ref": "#/definitions/entity" }
    },

    "balls": {
      "type": "array",
      "description": "Balls present at the start of the drill. Each holds exactly one lifecycle state.",
      "items": { "$ref": "#/definitions/ball" }
    },

    "frames": {
      "type": "array",
      "minItems": 1,
      "items": { "$ref": "#/definitions/frame" }
    },

    "areas": { "type": "array", "items": { "$ref": "#/definitions/area" } },
    "labels": { "type": "array", "items": { "$ref": "#/definitions/label" } }
  },

  "additionalProperties": false,

  "allOf": [
    {
      "if": {
        "type": "object",
        "properties": { "court": { "type": "object", "properties": { "ruleset": { "const": "custom" } } } }
      },
      "then": {
        "type": "object",
        "properties": { "court": { "type": "object", "required": ["custom_dimensions"] } }
      }
    },
    {
      "$comment": "sport absent OR basketball -> basketball action whitelist (default is a non-validating annotation, so absence must be handled explicitly for back-compat)",
      "if": {
        "anyOf": [
          { "not": { "required": ["sport"] } },
          { "properties": { "sport": { "const": "basketball" } } }
        ]
      },
      "then": {
        "type": "object",
        "properties": {
          "frames": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "actions": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": { "type": { "enum": ["move","cut","screen","defend","dribble","pass","shoot","rebound","pickup"] } }
                  }
                }
              }
            }
          }
        }
      }
    },
    {
      "if": { "required": ["sport"], "properties": { "sport": { "const": "soccer" } } },
      "then": {
        "type": "object",
        "properties": {
          "frames": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "actions": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": { "type": { "enum": ["move","pass","shoot","defend","dribble","tackle","clear"] } }
                  }
                }
              }
            }
          }
        }
      }
    },
    {
      "if": { "required": ["sport"], "properties": { "sport": { "const": "handball" } } },
      "then": {
        "type": "object",
        "properties": {
          "frames": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "actions": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": { "type": { "enum": ["move","pass","shoot","defend","cut","screen","pickup"] } }
                  }
                }
              }
            }
          }
        }
      }
    },
    {
      "if": { "required": ["sport"], "properties": { "sport": { "const": "hockey" } } },
      "then": {
        "type": "object",
        "properties": {
          "frames": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "actions": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": { "type": { "enum": ["move","pass","shoot","defend","dribble","clear","faceoff","check"] } }
                  }
                }
              }
            }
          }
        }
      }
    },
    {
      "if": { "required": ["sport"], "properties": { "sport": { "const": "futsal" } } },
      "then": {
        "type": "object",
        "properties": {
          "frames": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "actions": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": { "type": { "enum": ["move","pass","shoot","defend","dribble","tackle","clear"] } }
                  }
                }
              }
            }
          }
        }
      }
    }
  ]
}
