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 itemonAction(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>
)
}