{
  "manifest": {
    "schemaVersion": 1,
    "sdkVersion": "0.1.0",
    "id": "example.timeline-mixer",
    "version": "1.0.0",
    "name": "Timeline mixer",
    "description": "Mix individual video and audio clip levels in an interactive workspace, then review and apply the changes in the Editor.",
    "kind": "editor-extension",
    "runtime": "javascript",
    "license": "MIT",
    "provenance": {
      "notice": "MIT License\n\nCopyright (c) 2026 StillMade SDK contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and \nassociated documentation files (the \"Software\"), to deal in the Software without restriction, including \nwithout limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell \ncopies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the \nfollowing conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial \nportions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT \nLIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO \nEVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER \nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE \nUSE OR OTHER DEALINGS IN THE SOFTWARE.\n"
    },
    "inputs": {
      "timeline": {
        "type": "timeline",
        "primary": true,
        "context": "timeline"
      },
      "levels": {
        "type": "json",
        "default": []
      }
    },
    "outputs": {
      "edit": {
        "type": "timeline-edit",
        "primary": true
      }
    },
    "permissions": {
      "project": [
        "context.timeline.read",
        "timeline.propose"
      ]
    },
    "ui": []
  },
  "code": "const commands=[];\nfor(const collection of ['clips','audioItems'])for(const item of input.timeline[collection]||[]){\n const key=collection+':'+item.id;\n const level=input.levels.find(entry=>entry.key===key);if(!level)continue;\n const volume=level.volume;\n if(!Number.isFinite(volume)||volume<0||volume>200)throw new Error('Levels must be between 0 and 200 percent');\n if(volume!==(item.volume??100))commands.push({collection,operation:'volume',before:item,values:{volume}});\n}\nif(!commands.length)throw new Error('Change at least one clip level');\nif(commands.length>100)throw new Error('Mix at most 100 clips at a time');\nreturn {edit:{schemaVersion:1,title:'Mix clip levels',timelineId:input.timeline.activeTimelineId,commands}};",
  "view": {
    "html": "<section><h1>Timeline mixer</h1><p>Adjust clip levels. Review the proposed changes before applying them to your Editor.</p><p id=\"status\" role=\"status\"></p><div id=\"channels\"></div><button id=\"apply\" type=\"button\" data-stillmade-action=\"run\">Review mix</button><button id=\"reset\" type=\"button\">Reset levels</button></section>",
    "css": ".channel{display:grid;grid-template-columns:minmax(120px,1fr) minmax(100px,2fr) 72px;gap:12px;align-items:center;padding:12px 0;border-bottom:1px solid var(--border-tok)}button{margin:12px 8px 0 0}input{width:100%;min-width:0}h1{font-size:28px}@media(max-width:480px){.channel{grid-template-columns:1fr 72px}.channel strong{grid-column:1 / -1}}",
    "javascript": "const status=document.getElementById('status'),apply=document.getElementById('apply'),reset=document.getElementById('reset');\nlet current={},levels=[],rows=[],revision=0,busy=false;\nconst draw=()=>{\n StillMade.render('channels',rows.map((row,index)=>({tag:'div',className:'channel',children:[\n  {tag:'strong',text:row.name},\n  {tag:'input',id:'level-'+index,type:'range',min:0,max:200,step:1,value:levels.find(level=>level.key===row.key)?.volume??row.volume,action:'level:'+index,label:row.name+' volume',disabled:busy},\n  {tag:'output',id:'value-'+index,text:String(levels.find(level=>level.key===row.key)?.volume??row.volume)+'%'}\n ]})));\n apply.disabled=busy||rows.length===0;reset.disabled=busy||rows.length===0;\n};\nStillMade.onInput(input=>{\n current=input;revision+=1;levels=input.levels||[];\n rows=[...(input.timeline?.clips||[]).map(item=>({key:'clips:'+item.id,name:item.name||'Video clip',volume:item.volume??100})),...(input.timeline?.audioItems||[]).map(item=>({key:'audioItems:'+item.id,name:item.name||'Audio clip',volume:item.volume??100}))].slice(0,100);\n status.textContent=rows.length?'Adjust a level to start. Showing up to 100 clips.':'Add video or audio clips in the Editor to mix them here.';\n draw();\n});\nStillMade.onAction(event=>{\n if(busy||!event.action.startsWith('level:'))return;\n const index=Number(event.action.slice(6)),row=rows.find((item,i)=>i===index);if(!row)return;\n levels=[...levels.filter(level=>level.key!==row.key),{key:row.key,volume:Number(event.value)}];document.getElementById('value-'+index).textContent=event.value+'%';status.textContent='Unsaved mix. Review when ready.';\n});\nreset.addEventListener('click',()=>{if(busy)return;levels=[];status.textContent='Levels reset.';draw();});\napply.addEventListener('click',async()=>{\n if(busy)return;busy=true;const started=revision;draw();status.textContent='Preparing changes…';\n try{await StillMade.run({timeline:current.timeline,levels});if(started===revision)status.textContent='Mix ready. Review and apply the proposal below.';}\n catch(error){if(started===revision)status.textContent=error.message;}\n finally{busy=false;draw();}\n});"
  },
  "tests": [
    {
      "name": "Mix video and music without changing the timeline",
      "input": {
        "timeline": {
          "schemaVersion": 1,
          "activeTimelineId": "timeline-01",
          "clips": [
            {
              "id": "clip-1",
              "trackId": "video",
              "name": "Opening",
              "start": 0,
              "duration": 3,
              "volume": 100
            }
          ],
          "audioItems": [
            {
              "id": "audio-1",
              "trackId": "music",
              "name": "Music",
              "start": 0,
              "duration": 3,
              "volume": 70
            }
          ]
        },
        "levels": [
          {
            "key": "clips:clip-1",
            "volume": 80
          },
          {
            "key": "audioItems:audio-1",
            "volume": 30
          }
        ]
      },
      "expected": {
        "edit": {
          "schemaVersion": 1,
          "title": "Mix clip levels",
          "timelineId": "timeline-01",
          "commands": [
            {
              "collection": "clips",
              "operation": "volume",
              "before": {
                "id": "clip-1",
                "trackId": "video",
                "name": "Opening",
                "start": 0,
                "duration": 3,
                "volume": 100
              },
              "values": {
                "volume": 80
              }
            },
            {
              "collection": "audioItems",
              "operation": "volume",
              "before": {
                "id": "audio-1",
                "trackId": "music",
                "name": "Music",
                "start": 0,
                "duration": 3,
                "volume": 70
              },
              "values": {
                "volume": 30
              }
            }
          ]
        }
      }
    }
  ]
}