Video Studio SDKv0.0.3
UI

Components

The UI primitives re-exported through @studio-dev/vsdk and @studio-dev/vsdk/ui — use them in your panels for visual consistency with the editor.

The SDK re-exports a curated set of UI primitives from @studio-dev/vsdk-ui (the internal component library). They share the editor's theme tokens automatically, so anything you build with them slots into the design without extra styling work.

What's available

GroupComponents
Buttons & inputsButton, Input, Label, Checkbox, RadioGroup, Switch, Slider, Toggle, ToggleGroup
LayoutSeparator, ScrollArea, ScrollBar, Tabs (with TabsList, TabsTrigger, TabsContent)
OverlaysDialog, Sheet, Popover, Tooltip, DropdownMenu, ContextMenu
SelectionSelect
FeedbackProgress, Skeleton, Badge, Kbd, KbdGroup
CustomEditor-specific surfaces from ui-desktop/components (loading overlay, panels, etc.)

The Button and Badge exports also include their CVA variants (buttonVariants, badgeVariants) for composing custom triggers.

Importing

Two import paths give you the same set:

// Through the SDK barrel — recommended when you already import other SDK exports
import { Button, Input, Dialog, useStudioStore } from '@studio-dev/vsdk'

// Through the UI subpath — useful when you only need UI primitives
import { Button, Input, Dialog } from '@studio-dev/vsdk/ui'

A third option pulls a single component from the underlying library — best for plugin authors who want minimal coupling to the SDK barrel:

import { Button } from '@studio-dev/vsdk-ui/components/button'
import { Dialog, DialogContent } from '@studio-dev/vsdk-ui/components/dialog'

All three resolve to the same module. Pick one and be consistent within a plugin.

Why use these over your own?

  • Theme awareness — they read the studio CSS variables, so your panel changes color when the user switches light/dark or overrides themeOverrides.
  • Behavior — most are Radix/Base UI underneath, so accessibility (keyboard focus, ARIA, focus traps for modals) is handled.
  • Layout match — sizing and density match the editor's other surfaces — no "this panel feels bigger than everything else" problem.

Quick examples

Button (with CVA variants)

import { Button } from '@studio-dev/vsdk'
import { useStudioIcon } from '@studio-dev/vsdk'

function MyToolbar() {
  const PlusIcon = useStudioIcon('add')
  return (
    <div className="flex gap-2">
      <Button variant="default" size="sm">
        <PlusIcon className="size-4" />
        Add item
      </Button>
      <Button variant="secondary" size="sm">Secondary</Button>
      <Button variant="ghost" size="sm">Ghost</Button>
      <Button variant="destructive" size="sm">Delete</Button>
    </div>
  )
}

Variants: default, secondary, ghost, destructive, outline, link. Sizes: sm, default, lg, icon.

Input / Slider

import { Input, Label, Slider } from '@studio-dev/vsdk'

function OpacityControl({ value, onChange }: { value: number; onChange: (n: number) => void }) {
  return (
    <div className="flex flex-col gap-2">
      <Label htmlFor="opacity">Opacity</Label>
      <Slider
        id="opacity"
        min={0}
        max={1}
        step={0.01}
        value={[value]}
        onValueChange={([v]) => onChange(v)}
      />
      <Input
        type="number"
        min={0}
        max={1}
        step={0.01}
        value={value}
        onChange={(e) => onChange(Number(e.target.value))}
      />
    </div>
  )
}

Popover

import { Popover, PopoverTrigger, PopoverContent, Button } from '@studio-dev/vsdk'

<Popover>
  <PopoverTrigger asChild>
    <Button variant="ghost" size="sm">Options</Button>
  </PopoverTrigger>
  <PopoverContent className="w-56">
    {/* …menu items… */}
  </PopoverContent>
</Popover>

Tabs (good fit for plugin sub-views)

import { Tabs, TabsList, TabsTrigger, TabsContent } from '@studio-dev/vsdk'

<Tabs defaultValue="presets">
  <TabsList>
    <TabsTrigger value="presets">Presets</TabsTrigger>
    <TabsTrigger value="custom">Custom</TabsTrigger>
  </TabsList>
  <TabsContent value="presets">…</TabsContent>
  <TabsContent value="custom">…</TabsContent>
</Tabs>

Tooltip

import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@studio-dev/vsdk'

<TooltipProvider>
  <Tooltip>
    <TooltipTrigger asChild>
      <button>?</button>
    </TooltipTrigger>
    <TooltipContent>Helpful explanation</TooltipContent>
  </Tooltip>
</TooltipProvider>

Keyboard shortcut hint

import { Kbd, KbdGroup } from '@studio-dev/vsdk'

<KbdGroup>
  <Kbd>⌘</Kbd>
  <Kbd>Z</Kbd>
</KbdGroup>

Utility exports

A handful of helpers come along the same paths:

import {
  cn,                          // class concatenator (clsx + tailwind-merge)
  generateId, generateItemId,  // nanoid-based, prefixed
  framesToSeconds, secondsToFrames, framesToTimecode, framesToDisplayTime,
  timecodeToFrames, msToFrames, framesToMs, clampFrame,
} from '@studio-dev/vsdk'
HelperPurpose
cn(...classes)Merge class lists; preserves Tailwind precedence
generateId(prefix?)Random id like cmd_abc123
generateItemId / generateTrackId / generateMarkerId / generateRegionId / generateProjectId / generateAssetId / generateJobIdPrefixed ids for the corresponding entity
framesToSeconds(frames, fps)Frame → seconds
secondsToFrames(seconds, fps)Seconds → frame
framesToTimecode(frames, fps)"hh:mm:ss:ff"
framesToDisplayTime(frames, fps)"m:ss" (UI-friendly)
timecodeToFrames("hh:mm:ss:ff", fps)Inverse of framesToTimecode
msToFrames(ms, fps) / framesToMs(frames, fps)Millisecond bridge
clampFrame(frame, min, max)Clamp a frame number

Anchor for timeline-aware UI

If your panel needs to know where the user is on the timeline (e.g. "Insert at playhead"), prefer the SDK helpers over reimplementing logic:

import { findAvailableTrack, useStudioStore } from '@studio-dev/vsdk'

const tracks         = useStudioStore((s) => s.timeline.tracks)
const items          = useStudioStore((s) => s.timeline.items)
const itemIdsByTrack = useStudioStore((s) => s.timeline.itemIdsByTrack)
const currentFrame   = useStudioStore((s) => s.player.currentFrame)

const target = findAvailableTrack(tracks, itemIdsByTrack, items, 'track', currentFrame, 90)

That returns the first track of the given kind with room at the playhead — exactly what the filters/typography panels use when inserting.

On this page