Video Studio SDKv0.0.3
Plugin authoringExtension points

Context menu action

Right-click menu items on timeline clips — visibility, variants, shortcuts.

A context menu action appears in the right-click menu when the user has one or more timeline items selected. The action receives the array of selected item ids; you decide whether to show it (based on the selection) and what to do when invoked.

Registration shape

interface ContextMenuAction {
  id:        string
  label:     string
  icon?:     ComponentType<{ className?: string }>
  shortcut?: string                                  // displayed only — e.g. '⌘E'
  onAction:  (itemIds: string[]) => void
  isVisible?: (itemIds: string[]) => boolean
  variant?:  'default' | 'destructive'
  order:     number
}

Registering one

ctx.registerContextMenuAction({
  id:        'mycompany:fade-in',
  label:     'Add fade-in',
  icon:      ctx.icons.transition,
  order:     50,
  isVisible: (itemIds) => {
    const state = ctx.store.getState()
    return itemIds.every((id) => {
      const item = state.timeline.items[id]
      return item?.type === 'video' || item?.type === 'image'
    })
  },
  onAction: (itemIds) => {
    const state = ctx.store.getState()
    state.startBatch()
    for (const id of itemIds) {
      const item = state.timeline.items[id]
      if (!item) continue
      state.updateItemProperties(id, { style: { ...item.style, fadeInFrames: 30 } })
    }
    state.endBatch('Add fade-in')
  },
})

Field reference

id

Unique. Namespace it.

label / icon / shortcut

label is the menu text. icon is optional. shortcut is display-only — it does not register a hotkey. If you also want a hotkey, register one separately (see Hotkeys).

shortcut: '⌘E',     // shown on the right side of the menu item

onAction(itemIds)

Called when the user clicks the menu item. Receives the currently-selected item ids.

For multi-item actions, batch the mutations as one undoable entry:

onAction: (itemIds) => {
  const state = ctx.store.getState()
  state.startBatch()
  try {
    for (const id of itemIds) state.someMutation(id)
    state.endBatch('My action')
  } catch (err) {
    state.cancelBatch()
    throw err
  }
}

isVisible(itemIds)

Optional gate. Return true to render the action, false to hide it. Use it to limit the action to certain item types or selection sizes.

isVisible: (itemIds) => {
  if (itemIds.length === 0) return false
  const state = ctx.store.getState()
  return itemIds.every((id) => state.timeline.items[id]?.type === 'video')
}

If omitted the action is always shown. Plugins should almost always set isVisible — context-menu clutter is real, and items the action can't operate on shouldn't show it.

variant

  • 'default' (default) — normal menu item.
  • 'destructive' — rendered with destructive styling (red text). Use for delete / clear / strip operations.
variant: 'destructive',
label:   'Delete all keyframes',

order

Sort key inside the menu. Built-ins use 1 to 20; pick 50+ to slot after them.

Common patterns

Action that applies to a single item only

isVisible: (itemIds) => itemIds.length === 1,
onAction:  ([itemId]) => { /* … */ },

Action that requires a specific style field

Only show a "Remove animation" action when the item actually has animation:

isVisible: (itemIds) => {
  const state = ctx.store.getState()
  return itemIds.some((id) => state.timeline.items[id]?.animation)
}

Destructive action with confirmation

The action itself has no built-in confirm. Open a Dialog from a controlled state hook in your panel if you need one. For simple cases use window.confirm:

onAction: (itemIds) => {
  if (!window.confirm(`Delete ${itemIds.length} item(s)?`)) return
  const state = ctx.store.getState()
  state.startBatch()
  for (const id of itemIds) state.removeItem(id)
  state.endBatch('Delete items')
}

Rendering context menus in custom UI

Use useContextMenuActions() to get every registered action, then filter by isVisible:

import { useContextMenuActions } from '@studio-dev/vsdk'

function MyContextMenu({ selectedItemIds }: { selectedItemIds: string[] }) {
  const all = useContextMenuActions()
  const visible = all.filter((a) => a.isVisible?.(selectedItemIds) ?? true)

  return (
    <div>
      {visible.map((a) => (
        <button key={a.id} onClick={() => a.onAction(selectedItemIds)}>
          {a.label}{a.shortcut ? <span className="ml-auto">{a.shortcut}</span> : null}
        </button>
      ))}
    </div>
  )
}

On this page