summaryrefslogtreecommitdiff
path: root/.cursor
diff options
context:
space:
mode:
authorMaze <167211895+mazeincoding@users.noreply.github.com>2026-01-31 00:20:04 +0100
committerGitHub <noreply@github.com>2026-01-31 00:20:04 +0100
commit7bf0984698e43b9aa2dd879f65951318ed410390 (patch)
treeccf2cd590a47d550152afc07129cb55c71a9ec35 /.cursor
parent0173db99448d1c4c6415dcf15272e17551b48b04 (diff)
downloadopencut-7bf0984698e43b9aa2dd879f65951318ed410390.tar.gz
codebase overhaul (#697)
Diffstat (limited to '.cursor')
-rw-r--r--.cursor/rules/codebase-index.mdc1837
-rw-r--r--.cursor/rules/comments.mdc35
-rw-r--r--.cursor/rules/handling-uncertainty.mdc21
-rw-r--r--.cursor/rules/readability.mdc9
-rw-r--r--.cursor/rules/separation-of-concerns.mdc52
-rw-r--r--.cursor/rules/ultracite.mdc3
-rw-r--r--.cursor/rules/writing-scannable-code.mdc53
7 files changed, 2008 insertions, 2 deletions
diff --git a/.cursor/rules/codebase-index.mdc b/.cursor/rules/codebase-index.mdc
new file mode 100644
index 00000000..be1509dc
--- /dev/null
+++ b/.cursor/rules/codebase-index.mdc
@@ -0,0 +1,1837 @@
+---
+alwaysApply: true
+---
+
+# video-editor-oss Codebase Index
+
+**This file provides an index of exported functions, types, interfaces, classes, and constants in your codebase.**
+
+Updated in real-time by Twiggy. Use this to discover existing utilities and avoid duplicating code.
+
+## How to Use
+
+When implementing new features:
+1. Check if similar functionality already exists
+2. Reuse existing types and utilities
+3. Understand the API surface of your codebase
+
+```typescript
+## apps/web/src/constants
+
+editor-constants.ts
+ export const PLATFORM_LAYOUTS: Record<TPlatformLayout, string>
+ export const PANEL_CONFIG
+
+export-constants.ts
+ export const DEFAULT_EXPORT_OPTIONS
+ export const EXPORT_MIME_TYPES
+
+font-constants.ts
+ export interface FontOption {
+ value: string
+ label: string
+ category: "system" | "google" | "custom"
+ weights?: number[]
+ hasClassName?: boolean
+ }
+ export const FONT_OPTIONS: FontOption[]
+ export const DEFAULT_FONT
+ export type FontFamily = (typeof FONT_OPTIONS)[number]["value"]
+ export const getFontByValue = (value: string): FontOption | undefined => ...
+ export const getGoogleFonts = (): FontOption[] => ...
+ export const getSystemFonts = (): FontOption[] => ...
+
+language-constants.ts
+ export const LANGUAGES
+
+project-constants.ts
+ export const DEFAULT_CANVAS_PRESETS: TCanvasSize[]
+ export const FPS_PRESETS
+ export const BLUR_INTENSITY_PRESETS: { label: string; value: number }[]
+ export const DEFAULT_CANVAS_SIZE: TCanvasSize
+ export const DEFAULT_FPS
+ export const DEFAULT_BLUR_INTENSITY
+ export const DEFAULT_COLOR
+
+site-constants.ts
+ export const SITE_URL
+ export const SITE_INFO
+ export type ExternalTool = {
+ name: string;
+ description: string;
+ url: string;
+ icon: React.ElementType;
+ }
+ export const EXTERNAL_TOOLS: ExternalTool[]
+ export const DEFAULT_LOGO_URL
+ export const SOCIAL_LINKS
+ export type Sponsor = {
+ name: string;
+ url: string;
+ logo: string;
+ description: string;
+ }
+ export const SPONSORS: Sponsor[]
+
+stickers-constants.ts
+ export const STICKER_CATEGORIES
+ export const STICKER_CATEGORY_CONFIG: Record<
+ (typeof STICKER_CATEGORIES)[number],
+ string | undefined
+ >
+
+text-constants.ts
+ export const DEFAULT_TEXT_ELEMENT: Omit<TextElement, "id">
+
+timeline-constants.tsx
+ export const TRACK_COLORS: Record<TrackType, { background: string }>
+ export const TRACK_HEIGHTS: Record<TrackType, number>
+ export const TRACK_GAP
+ export const TIMELINE_CONSTANTS
+ export const DEFAULT_TIMELINE_VIEW_STATE: TTimelineViewState
+ export const TRACK_ICONS: Record<TrackType, React.ReactNode>
+
+transcription-constants.ts
+ export const TRANSCRIPTION_LANGUAGES
+ export const TRANSCRIPTION_MODELS: TranscriptionModel[]
+ export const DEFAULT_TRANSCRIPTION_MODEL: TranscriptionModelId
+ export const DEFAULT_CHUNK_LENGTH_SECONDS
+ export const DEFAULT_STRIDE_SECONDS
+ export const DEFAULT_WORDS_PER_CAPTION
+ export const MIN_CAPTION_DURATION_SECONDS
+
+## apps/web/src/core
+
+index.ts
+ export class EditorCore {
+ instance: EditorCore | null
+ command: CommandManager
+ playback: PlaybackManager
+ timeline: TimelineManager
+ scenes: ScenesManager
+ project: ProjectManager
+ media: MediaManager
+ renderer: RendererManager
+ save: SaveManager
+ audio: AudioManager
+ selection: SelectionManager
+ static getInstance(): EditorCore
+ static reset(): void
+ }
+
+## apps/web/src/hooks
+
+use-editor.ts
+ export function useEditor(): EditorCore
+
+use-file-upload.ts
+ export function useFileUpload({
+ accept,
+ multiple,
+ onFilesSelected,
+ }: UseFileUploadOptions = {})
+
+use-infinite-scroll.ts
+ export function useInfiniteScroll({
+ onLoadMore,
+ hasMore,
+ isLoading,
+ threshold = 200,
+ enabled = true,
+ }: UseInfiniteScrollOptions)
+
+use-keybindings.ts
+ export function useKeybindingsListener()
+ export function useKeybindingDisabler()
+
+use-keyboard-shortcuts-help.ts
+ export interface KeyboardShortcut {
+ id: string
+ keys: string[]
+ description: string
+ category: string
+ action: TAction
+ icon?: React.ReactNode
+ }
+ export function useKeyboardShortcutsHelp()
+
+use-mobile.ts
+ export function useIsMobile()
+
+use-raf-loop.ts
+ export function useRafLoop(callback: ({ time }: { time: number }) => void)
+
+use-reveal-item.ts
+ export function useRevealItem(
+ highlightId: string | null,
+ onClearHighlight: () => void,
+ highlightDuration = 1000,
+ )
+
+use-sound-search.ts
+ export function useSoundSearch({
+ query,
+ commercialOnly,
+ }: {
+ query: string;
+ commercialOnly: boolean;
+ })
+
+## apps/web/src/hooks/actions
+
+use-action-handler.ts
+ export function useActionHandler(
+ action: A,
+ handler: TActionFunc<A>,
+ isActive: TActionHandlerOptions,
+ )
+
+use-editor-actions.ts
+ export function useEditorActions()
+
+## apps/web/src/hooks/storage
+
+use-local-storage.ts
+ export function useLocalStorage({
+ key,
+ defaultValue,
+ }: {
+ key: string;
+ defaultValue: T;
+ }): [
+ T,
+ ({ value }: { value: T | ((previousValue: T) => T) }) => void,
+ boolean,
+ ]
+
+## apps/web/src/hooks/timeline
+
+use-edge-auto-scroll.ts
+ export function useEdgeAutoScroll({
+ isActive,
+ getMouseClientX,
+ rulerScrollRef,
+ tracksScrollRef,
+ contentWidth,
+ edgeThreshold = 100,
+ maxScrollSpeed = 15,
+ }: UseEdgeAutoScrollParams): void
+
+use-scroll-sync.ts
+ export function useScrollSync({
+ tracksScrollRef,
+ rulerScrollRef,
+ trackLabelsScrollRef,
+ bookmarksScrollRef,
+ }: UseScrollSyncProps)
+
+use-selection-box.ts
+ export function useSelectionBox({
+ containerRef,
+ onSelectionComplete,
+ isEnabled = true,
+ tracksScrollRef,
+ zoomLevel,
+ }: UseSelectionBoxProps)
+
+use-snap-indicator-position.ts
+ export function useSnapIndicatorPosition({
+ snapPoint,
+ zoomLevel,
+ tracks,
+ timelineRef,
+ trackLabelsRef,
+ tracksScrollRef,
+ }: UseSnapIndicatorPositionParams): SnapIndicatorPosition
+
+use-timeline-drag-drop.ts
+ export function useTimelineDragDrop({
+ containerRef,
+ headerRef,
+ zoomLevel,
+ }: UseTimelineDragDropProps)
+
+use-timeline-playhead.ts
+ export function useTimelinePlayhead({
+ zoomLevel,
+ rulerRef,
+ rulerScrollRef,
+ tracksScrollRef,
+ playheadRef,
+ }: UseTimelinePlayheadProps)
+
+use-timeline-seek.ts
+ export function useTimelineSeek({
+ playheadRef,
+ trackLabelsRef,
+ rulerScrollRef,
+ tracksScrollRef,
+ zoomLevel,
+ duration,
+ isSelecting,
+ clearSelectedElements,
+ seek,
+ }: UseTimelineSeekProps)
+
+use-timeline-snapping.ts
+ export interface SnapPoint {
+ time: number
+ type: "element-start" | "element-end" | "playhead"
+ elementId?: string
+ trackId?: string
+ }
+ export interface SnapResult {
+ snappedTime: number
+ snapPoint: SnapPoint | null
+ snapDistance: number
+ }
+ export interface UseTimelineSnappingOptions {
+ snapThreshold?: number
+ enableElementSnapping?: boolean
+ enablePlayheadSnapping?: boolean
+ }
+ export function useTimelineSnapping({
+ snapThreshold = 10,
+ enableElementSnapping = true,
+ enablePlayheadSnapping = true,
+ }: UseTimelineSnappingOptions = {})
+
+use-timeline-zoom.ts
+ export function useTimelineZoom({
+ containerRef,
+ minZoom = TIMELINE_CONSTANTS.ZOOM_MIN,
+ initialZoom,
+ initialScrollLeft,
+ initialPlayheadTime,
+ tracksScrollRef,
+ rulerScrollRef,
+ }: UseTimelineZoomProps): UseTimelineZoomReturn
+
+## apps/web/src/hooks/timeline/element
+
+use-element-interaction.ts
+ export function useElementInteraction({
+ zoomLevel,
+ timelineRef,
+ tracksContainerRef,
+ tracksScrollRef,
+ headerRef,
+ snappingEnabled,
+ onSnapPointChange,
+ }: UseElementInteractionProps)
+
+use-element-resize.ts
+ export interface ResizeState {
+ elementId: string
+ side: "left" | "right"
+ startX: number
+ initialTrimStart: number
+ initialTrimEnd: number
+ initialStartTime: number
+ initialDuration: number
+ }
+ export function useTimelineElementResize({
+ element,
+ track,
+ zoomLevel,
+ onSnapPointChange,
+ onResizeStateChange,
+ }: UseTimelineElementResizeProps)
+
+use-element-selection.ts
+ export function useElementSelection()
+
+## apps/web/src/lib
+
+drag-data.ts
+ export function setDragData({
+ dataTransfer,
+ dragData,
+ }: {
+ dataTransfer: DataTransfer;
+ dragData: TimelineDragData;
+ }): void
+ export function getDragData({
+ dataTransfer,
+ }: {
+ dataTransfer: DataTransfer;
+ }): TimelineDragData | null
+ export function hasDragData({
+ dataTransfer,
+ }: {
+ dataTransfer: DataTransfer;
+ }): boolean
+ export function clearDragData(): void
+
+export.ts
+ export function getExportMimeType({
+ format,
+ }: {
+ format: ExportFormat;
+ }): string
+ export function getExportFileExtension({
+ format,
+ }: {
+ format: ExportFormat;
+ }): string
+
+iconify-api.ts
+ export const ICONIFY_HOSTS
+ export interface IconSet {
+ prefix: string
+ name: string
+ total: number
+ author?: {
+ name: string;
+ url?: string;
+ }
+ license?: {
+ title: string;
+ spdx?: string;
+ url?: string;
+ }
+ samples?: string[]
+ category?: string
+ palette?: boolean
+ }
+ export interface IconSearchResult {
+ icons: string[]
+ total: number
+ limit: number
+ start: number
+ collections: Record<string, IconSet>
+ }
+ export interface CollectionInfo {
+ prefix: string
+ total: number
+ title?: string
+ uncategorized?: string[]
+ categories?: Record<string, string[]>
+ hidden?: string[]
+ aliases?: Record<string, string>
+ }
+ export function getCollections(
+ category?: string,
+ ): Promise<Record<string, IconSet>>
+ export function getCollection(
+ prefix: string,
+ ): Promise<CollectionInfo | null>
+ export function searchIcons(
+ query: string,
+ limit: number = 64,
+ prefixes?: string[],
+ category?: string,
+ ): Promise<IconSearchResult>
+ export function buildIconSvgUrl(
+ host: string,
+ iconName: string,
+ params?: {
+ color?: string;
+ width?: number;
+ height?: number;
+ flip?: "horizontal" | "vertical" | "horizontal,vertical";
+ rotate?: number | string;
+ },
+ ): string
+ export function getIconSvgUrl(
+ iconName: string,
+ params?: Parameters<typeof buildIconSvgUrl>[2],
+ ): string
+ export function downloadSvgAsText(
+ iconName: string,
+ params?: Parameters<typeof getIconSvgUrl>[1],
+ ): Promise<string>
+ export function svgToFile(svgText: string, fileName: string): File
+ export const POPULAR_COLLECTIONS
+ export function getCategoriesFromCollections(
+ collections: Record<string, IconSet>,
+ ): string[]
+
+rate-limit.ts
+ export const baseRateLimit
+ export function checkRateLimit({ request }: { request: Request })
+
+scenes.ts
+ export function getMainScene({ scenes }: { scenes: TScene[] }): TScene | null
+ export function ensureMainScene({ scenes }: { scenes: TScene[] }): TScene[]
+ export function buildDefaultScene({
+ name,
+ isMain,
+ }: {
+ name: string;
+ isMain: boolean;
+ }): TScene
+ export function canDeleteScene({ scene }: { scene: TScene }): {
+ canDelete: boolean;
+ reason?: string;
+ }
+ export function getFallbackSceneAfterDelete({
+ scenes,
+ deletedSceneId,
+ currentSceneId,
+ }: {
+ scenes: TScene[];
+ deletedSceneId: string;
+ currentSceneId: string | null;
+ }): TScene | null
+ export function findCurrentScene({
+ scenes,
+ currentSceneId,
+ }: {
+ scenes: TScene[];
+ currentSceneId: string;
+ }): TScene | null
+ export function getProjectDurationFromScenes({
+ scenes,
+ }: {
+ scenes: TScene[];
+ }): number
+ export function updateSceneInArray({
+ scenes,
+ sceneId,
+ updates,
+ }: {
+ scenes: TScene[];
+ sceneId: string;
+ updates: Partial<TScene>;
+ }): TScene[]
+
+time.ts
+ export function roundToFrame({
+ time,
+ fps,
+ }: {
+ time: number;
+ fps: number;
+ }): number
+ export function formatTimeCode({
+ timeInSeconds,
+ format = "HH:MM:SS:CS",
+ fps,
+ }: {
+ timeInSeconds: number;
+ format?: TTimeCode;
+ fps?: number;
+ }): string
+ export function parseTimeCode({
+ timeCode,
+ format = "HH:MM:SS:CS",
+ fps,
+ }: {
+ timeCode: string;
+ format?: TTimeCode;
+ fps: number;
+ }): number | null
+ export function guessTimeCodeFormat({
+ timeCode,
+ }: {
+ timeCode: string;
+ }): TTimeCode | null
+ export function timeToFrame({
+ time,
+ fps,
+ }: {
+ time: number;
+ fps: number;
+ }): number
+ export function frameToTime({
+ frame,
+ fps,
+ }: {
+ frame: number;
+ fps: number;
+ }): number
+ export function snapTimeToFrame({
+ time,
+ fps,
+ }: {
+ time: number;
+ fps: number;
+ }): number
+ export function getSnappedSeekTime({
+ rawTime,
+ duration,
+ fps,
+ }: {
+ rawTime: number;
+ duration: number;
+ fps: number;
+ }): number
+ export function getLastFrameTime({
+ duration,
+ fps,
+ }: {
+ duration: number;
+ fps: number;
+ }): number
+
+## apps/web/src/lib/actions
+
+definitions.ts
+ export type TActionCategory = | "playback"
+ | "navigation"
+ | "editing"
+ | "selection"
+ | "history"
+ | "timeline"
+ | "controls"
+ export interface TActionDefinition {
+ description: string
+ category: TActionCategory
+ defaultShortcuts?: ShortcutKey[]
+ args?: Record<string, unknown>
+ }
+ export const ACTIONS
+ export type TAction = keyof typeof ACTIONS
+ export function getActionDefinition(action: TAction): TActionDefinition
+ export function getDefaultShortcuts(): Record<ShortcutKey, TAction>
+
+registry.ts
+ export function bindAction(
+ action: A,
+ handler: TActionFunc<A>,
+ )
+ export function unbindAction(
+ action: A,
+ handler: TActionFunc<A>,
+ )
+ export const invokeAction = (
+ action: A,
+ args?: TArgOfAction<A>,
+ trigger?: TInvocationTrigger,
+ ) => ...
+
+types.ts
+ export type TActionArgsMap = {
+ "seek-forward": { seconds: number } | undefined;
+ "seek-backward": { seconds: number } | undef...
+ export type TActionWithArgs = keyof TActionArgsMap
+ export type TActionWithOptionalArgs = | TActionWithNoArgs
+ | TKeysWithValueUndefined<TActionArgsMap>
+ export type TActionWithNoArgs = Exclude<TAction, TActionWithArgs>
+ export type TArgOfAction<A extends TAction> = A extends TActionWithArgs
+ ? TActionArgsMap[A]
+ : undefined
+ export type TActionFunc<A extends TAction> = A extends TActionWithArgs
+ ? (arg: TArgOfAction<A>, trigger?: TInvocationTrigger) => void
+ : (_?:...
+ export type TInvocationTrigger = "keypress" | "mouseclick"
+ export type TBoundActionList = {
+ [A in TAction]?: Array<TActionFunc<A>>;
+ }
+ export type TActionHandlerOptions = | MutableRefObject<boolean>
+ | boolean
+ | undefined
+
+## apps/web/src/lib/auth
+
+server.ts
+ export const auth
+ export type Auth = typeof auth
+
+## apps/web/src/lib/blog
+
+query.ts
+ export function getPosts()
+ export function getTags()
+ export function getSinglePost({ slug }: { slug: string })
+ export function getCategories()
+ export function getAuthors()
+ export function processHtmlContent({
+ html,
+ }: {
+ html: string;
+ }): Promise<string>
+
+## apps/web/src/lib/db
+
+index.ts
+ export const db
+
+schema.ts
+ export const users
+ export const sessions
+ export const accounts
+ export const verifications
+
+## apps/web/src/lib/gradients
+
+canvas.ts
+ export function drawCssBackground({
+ ctx,
+ width,
+ height,
+ css,
+ }: {
+ ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
+ width: number;
+ height: number;
+ css: string;
+ }): void
+
+parser.ts
+ export type GradientOrientation = LinearOrientation | Array<RadialOrientation>
+ export type Color = | { type: "hex"; value: string }
+ | { type: "literal"; value: string }
+ | { type: "rgb"; value: A...
+ export type ColorStop = Color & { length?: Distance }
+ export type GradientAst = {
+ type: GradientType;
+ orientation: GradientOrientation | undefined;
+ colorStops: Array<ColorSto...
+ export const parseGradient = ({
+ code,
+ }: {
+ code: string;
+ }): Array<GradientAst> => ...
+ export const GradientParser
+
+## apps/web/src/lib/media
+
+audio.ts
+ export type CollectedAudioElement = Omit<
+ AudioElement,
+ "type" | "mediaId" | "volume" | "id" | "name" | "sourceType" | "sourceUrl"
+ ...
+ export function createAudioContext(): AudioContext
+ export interface DecodedAudio {
+ samples: Float32Array
+ sampleRate: number
+ }
+ export function decodeAudioToFloat32({
+ audioBlob,
+ }: {
+ audioBlob: Blob;
+ }): Promise<DecodedAudio>
+ export function collectAudioElements({
+ tracks,
+ mediaAssets,
+ audioContext,
+ }: {
+ tracks: TimelineTrack[];
+ mediaAssets: MediaAsset[];
+ audioContext: AudioContext;
+ }): Promise<CollectedAudioElement[]>
+ export interface AudioClipSource {
+ id: string
+ sourceKey: string
+ file: File
+ startTime: number
+ duration: number
+ trimStart: number
+ trimEnd: number
+ muted: boolean
+ }
+ export function collectAudioMixSources({
+ tracks,
+ mediaAssets,
+ }: {
+ tracks: TimelineTrack[];
+ mediaAssets: MediaAsset[];
+ }): Promise<AudioMixSource[]>
+ export function collectAudioClips({
+ tracks,
+ mediaAssets,
+ }: {
+ tracks: TimelineTrack[];
+ mediaAssets: MediaAsset[];
+ }): Promise<AudioClipSource[]>
+ export function createTimelineAudioBuffer({
+ tracks,
+ mediaAssets,
+ duration,
+ sampleRate = 44100,
+ audioContext,
+ }: {
+ tracks: TimelineTrack[];
+ mediaAssets: MediaAsset[];
+ duration: number;
+ sampleRate?: number;
+ audioContext?: AudioContext;
+ }): Promise<AudioBuffer | null>
+
+media-utils.ts
+ export const SUPPORTS_AUDIO: readonly MediaType[]
+ export function mediaSupportsAudio({
+ media,
+ }: {
+ media: MediaAsset | null | undefined;
+ }): boolean
+ export const getMediaTypeFromFile = ({
+ file,
+ }: {
+ file: File;
+ }): MediaType | null => ...
+
+mediabunny.ts
+ export function getVideoInfo({
+ videoFile,
+ }: {
+ videoFile: File;
+ }): Promise<{
+ duration: number;
+ width: number;
+ height: number;
+ fps: number;
+ }>
+ export const extractTimelineAudio = ({
+ tracks,
+ mediaAssets,
+ totalDuration,
+ onProgress,
+ }: {
+ tracks: TimelineTrack[];
+ mediaAssets: MediaAsset[];
+ totalDuration: number;
+ onProgress?: (progress: number) => void;
+ }): Promise<Blob> => ...
+
+processing.ts
+ export interface ProcessedMediaAsset extends Omit<MediaAsset, "id">
+ export function generateThumbnail({
+ videoFile,
+ timeInSeconds,
+ }: {
+ videoFile: File;
+ timeInSeconds: number;
+ }): Promise<string>
+ export function generateImageThumbnail({
+ imageFile,
+ }: {
+ imageFile: File;
+ }): Promise<string>
+ export function processMediaAssets({
+ files,
+ onProgress,
+ }: {
+ files: FileList | File[];
+ onProgress?: ({ progress }: { progress: number }) => void;
+ }): Promise<ProcessedMediaAsset[]>
+
+## apps/web/src/lib/timeline
+
+bookmarks.ts
+ export function findBookmarkIndex({
+ bookmarks,
+ frameTime,
+ }: {
+ bookmarks: number[];
+ frameTime: number;
+ }): number
+ export function isBookmarkAtTime({
+ bookmarks,
+ frameTime,
+ }: {
+ bookmarks: number[];
+ frameTime: number;
+ }): boolean
+ export function toggleBookmarkInArray({
+ bookmarks,
+ frameTime,
+ }: {
+ bookmarks: number[];
+ frameTime: number;
+ }): number[]
+ export function removeBookmarkFromArray({
+ bookmarks,
+ frameTime,
+ }: {
+ bookmarks: number[];
+ frameTime: number;
+ }): number[]
+ export function getFrameTime({
+ time,
+ fps,
+ }: {
+ time: number;
+ fps: number;
+ }): number
+
+drop-utils.ts
+ export function computeDropTarget({
+ elementType,
+ mouseX,
+ mouseY,
+ tracks,
+ playheadTime,
+ isExternalDrop,
+ elementDuration,
+ pixelsPerSecond,
+ zoomLevel,
+ verticalDragDirection,
+ startTimeOverride,
+ excludeElementId,
+ }: ComputeDropTargetParams): DropTarget
+ export function getDropLineY({
+ dropTarget,
+ tracks,
+ }: {
+ dropTarget: DropTarget;
+ tracks: TimelineTrack[];
+ }): number
+
+element-utils.ts
+ export function canElementHaveAudio(
+ element: TimelineElement,
+ )
+ export function canElementBeHidden(
+ element: TimelineElement,
+ )
+ export function hasMediaId(
+ element: TimelineElement,
+ )
+ export function requiresMediaId({
+ element,
+ }: {
+ element: CreateTimelineElement;
+ }): boolean
+ export function checkElementOverlaps({
+ elements,
+ }: {
+ elements: TimelineElement[];
+ }): boolean
+ export function resolveElementOverlaps({
+ elements,
+ }: {
+ elements: TimelineElement[];
+ }): TimelineElement[]
+ export function wouldElementOverlap({
+ elements,
+ startTime,
+ endTime,
+ excludeElementId,
+ }: {
+ elements: TimelineElement[];
+ startTime: number;
+ endTime: number;
+ excludeElementId?: string;
+ }): boolean
+ export function buildTextElement({
+ raw,
+ startTime,
+ }: {
+ raw: Partial<Omit<TextElement, "type" | "id">>;
+ startTime: number;
+ }): CreateTimelineElement
+ export function buildStickerElement({
+ iconName,
+ startTime,
+ }: {
+ iconName: string;
+ startTime: number;
+ }): CreateStickerElement
+ export function buildVideoElement({
+ mediaId,
+ name,
+ duration,
+ startTime,
+ }: {
+ mediaId: string;
+ name: string;
+ duration: number;
+ startTime: number;
+ }): CreateVideoElement
+ export function buildImageElement({
+ mediaId,
+ name,
+ duration,
+ startTime,
+ }: {
+ mediaId: string;
+ name: string;
+ duration: number;
+ startTime: number;
+ }): CreateImageElement
+ export function buildUploadAudioElement({
+ mediaId,
+ name,
+ duration,
+ startTime,
+ buffer,
+ }: {
+ mediaId: string;
+ name: string;
+ duration: number;
+ startTime: number;
+ buffer?: AudioBuffer;
+ }): CreateUploadAudioElement
+ export function buildLibraryAudioElement({
+ sourceUrl,
+ name,
+ duration,
+ startTime,
+ buffer,
+ }: {
+ sourceUrl: string;
+ name: string;
+ duration: number;
+ startTime: number;
+ buffer?: AudioBuffer;
+ }): CreateLibraryAudioElement
+ export function getElementsAtTime({
+ tracks,
+ time,
+ }: {
+ tracks: TimelineTrack[];
+ time: number;
+ }): { trackId: string; elementId: string }[]
+
+index.ts
+ export function calculateTotalDuration({
+ tracks,
+ }: {
+ tracks: TimelineTrack[];
+ }): number
+
+ruler-utils.ts
+ export interface RulerConfig {
+ labelIntervalSeconds: number
+ tickIntervalSeconds: number
+ }
+ export function getRulerConfig({
+ zoomLevel,
+ fps,
+ }: {
+ zoomLevel: number;
+ fps: number;
+ }): RulerConfig
+ export function shouldShowLabel({
+ time,
+ labelIntervalSeconds,
+ }: {
+ time: number;
+ labelIntervalSeconds: number;
+ }): boolean
+ export function formatRulerLabel({
+ timeInSeconds,
+ fps,
+ }: {
+ timeInSeconds: number;
+ fps: number;
+ }): string
+
+track-utils.ts
+ export function canTracktHaveAudio(
+ track: TimelineTrack,
+ )
+ export function canTrackBeHidden(
+ track: TimelineTrack,
+ )
+ export function getTrackColor({ type }: { type: TrackType })
+ export function getTrackClasses({ type }: { type: TrackType })
+ export function getTrackHeight({ type }: { type: TrackType }): number
+ export function getCumulativeHeightBefore({
+ tracks,
+ trackIndex,
+ }: {
+ tracks: Array<{ type: TrackType }>;
+ trackIndex: number;
+ }): number
+ export function getTotalTracksHeight({
+ tracks,
+ }: {
+ tracks: Array<{ type: TrackType }>;
+ }): number
+ export function buildEmptyTrack({
+ id,
+ type,
+ name,
+ }: {
+ id: string;
+ type: TrackType;
+ name?: string;
+ }): TimelineTrack
+ export function getDefaultInsertIndexForTrack({
+ tracks,
+ trackType,
+ }: {
+ tracks: TimelineTrack[];
+ trackType: TrackType;
+ }): number
+ export function getHighestInsertIndexForTrack({
+ tracks,
+ trackType,
+ }: {
+ tracks: TimelineTrack[];
+ trackType: TrackType;
+ }): number
+ export function isMainTrack(track: TimelineTrack)
+ export function getMainTrack({
+ tracks,
+ }: {
+ tracks: TimelineTrack[];
+ }): TimelineTrack | null
+ export function ensureMainTrack({
+ tracks,
+ }: {
+ tracks: TimelineTrack[];
+ }): TimelineTrack[]
+ export function canElementGoOnTrack({
+ elementType,
+ trackType,
+ }: {
+ elementType: ElementType;
+ trackType: TrackType;
+ }): boolean
+ export function validateElementTrackCompatibility({
+ element,
+ track,
+ }: {
+ element: { type: ElementType };
+ track: { type: TrackType };
+ }): { isValid: boolean; errorMessage?: string }
+
+zoom-utils.ts
+ export function getTimelineZoomMin({
+ duration,
+ containerWidth,
+ }: {
+ duration: number;
+ containerWidth: number | null | undefined;
+ }): number
+ export function getTimelinePaddingPx({
+ containerWidth,
+ zoomLevel,
+ minZoom,
+ }: {
+ containerWidth: number;
+ zoomLevel: number;
+ minZoom: number;
+ }): number
+ export function getZoomPercent({
+ zoomLevel,
+ minZoom,
+ }: {
+ zoomLevel: number;
+ minZoom: number;
+ }): number
+
+## apps/web/src/lib/transcription
+
+caption.ts
+ export function buildCaptionChunks({
+ segments,
+ wordsPerChunk = DEFAULT_WORDS_PER_CAPTION,
+ minDuration = MIN_CAPTION_DURATION_SECONDS,
+ }: {
+ segments: TranscriptionSegment[];
+ wordsPerChunk?: number;
+ minDuration?: number;
+ }): CaptionChunk[]
+
+## apps/web/src/services/media
+
+video-cache.ts
+ export class VideoCache {
+ sinks
+ initPromises
+ async getFrameAt({
+ mediaId,
+ file,
+ time,
+ }: {
+ mediaId: string;
+ file: File;
+ time: number;
+ }): Promise<WrappedCanvas | null>
+ clearVideo({ mediaId }: { mediaId: string }): void
+ clearAll(): void
+ getStats()
+ }
+ export const videoCache
+
+## apps/web/src/services/renderer
+
+canvas-renderer.ts
+ export type CanvasRendererParams = {
+ width: number;
+ height: number;
+ fps: number;
+ }
+ export class CanvasRenderer {
+ canvas: OffscreenCanvas | HTMLCanvasElement
+ context: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D
+ width: number
+ height: number
+ fps: number
+ constructor({ width, height, fps }: CanvasRendererParams)
+ setSize({ width, height }: { width: number; height: number })
+ async render({ node, time }: { node: BaseNode; time: number })
+ async renderToCanvas({
+ node,
+ time,
+ targetCanvas,
+ }: {
+ node: BaseNode;
+ time: number;
+ targetCanvas: HTMLCanvasElement;
+ })
+ }
+
+scene-builder.ts
+ export type BuildSceneParams = {
+ canvasSize: TCanvasSize;
+ tracks: TimelineTrack[];
+ mediaAssets: MediaAsset[];
+ duration: numb...
+ export function buildScene(params: BuildSceneParams)
+
+scene-exporter.ts
+ export type ExportFormat = "mp4" | "webm"
+ export type ExportQuality = "low" | "medium" | "high" | "very_high"
+ export type SceneExporterEvents = {
+ progress: [progress: number];
+ complete: [buffer: ArrayBuffer];
+ error: [error: Error];
+ cance...
+ export class SceneExporter extends EventEmitter<SceneExporterEvents> {
+ renderer: CanvasRenderer
+ format: ExportFormat
+ quality: ExportQuality
+ shouldIncludeAudio: boolean
+ audioBuffer: AudioBuffer
+ isCancelled
+ constructor({
+ width,
+ height,
+ fps,
+ format,
+ quality,
+ shouldIncludeAudio,
+ audioBuffer,
+ }: ExportParams)
+ cancel(): void
+ async export({
+ rootNode,
+ }: {
+ rootNode: RootNode;
+ }): Promise<ArrayBuffer | null>
+ }
+
+## apps/web/src/services/storage
+
+indexeddb-adapter.ts
+ export class IndexedDBAdapter implements StorageAdapter<T> {
+ dbName: string
+ storeName: string
+ version: number
+ constructor(dbName: string, storeName: string, version = 1)
+ async get(key: string): Promise<T | null>
+ async set(key: string, value: T): Promise<void>
+ async remove(key: string): Promise<void>
+ async list(): Promise<string[]>
+ async getAll(): Promise<T[]>
+ async clear(): Promise<void>
+ }
+
+opfs-adapter.ts
+ export class OPFSAdapter implements StorageAdapter<File> {
+ directoryName: string
+ constructor(directoryName = "media")
+ async get(key: string): Promise<File | null>
+ async set(key: string, file: File): Promise<void>
+ async remove(key: string): Promise<void>
+ async list(): Promise<string[]>
+ async clear(): Promise<void>
+ static isSupported(): boolean
+ }
+
+storage-service.ts
+ export const storageService
+
+types.ts
+ export interface StorageAdapter<T> {
+ get(key: string): Promise<T | null>
+ set(key: string, value: T): Promise<void>
+ remove(key: string): Promise<void>
+ list(): Promise<string[]>
+ clear(): Promise<void>
+ }
+ export interface MediaAssetData {
+ id: string
+ name: string
+ type: MediaType
+ size: number
+ lastModified: number
+ width?: number
+ height?: number
+ duration?: number
+ fps?: number
+ ephemeral?: boolean
+ thumbnailUrl?: string
+ sourceStickerIconName?: string
+ }
+ export type SerializedScene = Omit<TScene, "createdAt" | "updatedAt"> & {
+ createdAt: string;
+ updatedAt: string;
+ }
+ export type SerializedProjectMetadata = Omit<
+ TProjectMetadata,
+ "createdAt" | "updatedAt"
+ > & {
+ createdAt: string;
+ updatedAt: string;
+ }
+ export type SerializedProject = Omit<TProject, "metadata" | "scenes"> & {
+ metadata: SerializedProjectMetadata;
+ scenes: Serializ...
+ export interface StorageConfig {
+ projectsDb: string
+ mediaDb: string
+ savedSoundsDb: string
+ version: number
+ }
+
+## apps/web/src/services/transcription
+
+index.ts
+ export const transcriptionService
+
+worker.ts
+ export type WorkerMessage = | { type: "init"; modelId: string }
+ | { type: "transcribe"; audio: Float32Array; language: strin...
+ export type WorkerResponse = | { type: "init-progress"; progress: number }
+ | { type: "init-complete" }
+ | { type: "init-error...
+
+## apps/web/src/stores
+
+assets-panel-store.tsx
+ export const TAB_KEYS
+ export type Tab = (typeof TAB_KEYS)[number]
+ export const tabs
+ export const useAssetsPanelStore
+
+editor-store.ts
+ export const useEditorStore
+
+keybindings-store.ts
+ export const defaultKeybindings: KeybindingConfig
+ export interface KeybindingConflict {
+ key: ShortcutKey
+ existingAction: TActionWithOptionalArgs
+ newAction: TActionWithOptionalArgs
+ }
+ export const useKeybindingsStore
+
+panel-store.ts
+ export interface PanelSizes {
+ tools: number
+ preview: number
+ properties: number
+ mainContent: number
+ timeline: number
+ }
+ export type PanelId = keyof PanelSizes
+ export const usePanelStore
+
+sounds-store.ts
+ export const useSoundsStore
+
+stickers-store.ts
+ export const useStickersStore
+
+text-properties-store.ts
+ export type TextPropertiesTab = "text" | "transform"
+ export interface TextPropertiesTabMeta {
+ value: TextPropertiesTab
+ label: string
+ }
+ export const TEXT_PROPERTIES_TABS: ReadonlyArray<TextPropertiesTabMeta>
+ export function isTextPropertiesTab(value: string)
+ export const useTextPropertiesStore
+
+timeline-store.ts
+ export const useTimelineStore
+
+## apps/web/src/types
+
+assets.ts
+ export type MediaType = "image" | "video" | "audio"
+ export interface MediaAsset extends Omit<MediaAssetData, "size" | "lastModified"> {
+ file: File
+ url?: string
+ }
+
+blog.ts
+ export type Post = {
+ id: string;
+ slug: string;
+ title: string;
+ content: string;
+ description: string;
+ coverImage...
+ export type Pagination = {
+ limit: number;
+ currpage: number;
+ nextPage: number | null;
+ prevPage: number | null;
+ totalIt...
+ export type MarblePostList = {
+ posts: Post[];
+ pagination: Pagination;
+ }
+ export type MarblePost = {
+ post: Post;
+ }
+ export type Tag = {
+ id: string;
+ name: string;
+ slug: string;
+ }
+ export type MarbleTag = {
+ tag: Tag;
+ }
+ export type MarbleTagList = {
+ tags: Tag[];
+ pagination: Pagination;
+ }
+ export type Category = {
+ id: string;
+ name: string;
+ slug: string;
+ }
+ export type MarbleCategory = {
+ category: Category;
+ }
+ export type MarbleCategoryList = {
+ categories: Category[];
+ pagination: Pagination;
+ }
+ export type Author = {
+ id: string;
+ name: string;
+ image: string;
+ }
+ export type MarbleAuthor = {
+ author: Author;
+ }
+ export type MarbleAuthorList = {
+ authors: Author[];
+ pagination: Pagination;
+ }
+
+drag.ts
+ export interface MediaDragData extends BaseDragData {
+ type: "media"
+ mediaType: "image" | "video" | "audio"
+ }
+ export interface TextDragData extends BaseDragData {
+ type: "text"
+ content: string
+ }
+ export interface StickerDragData extends BaseDragData {
+ type: "sticker"
+ iconName: string
+ }
+ export type TimelineDragData = MediaDragData | TextDragData | StickerDragData
+
+editor.ts
+ export type TPlatformLayout = "tiktok"
+
+export.ts
+ export const EXPORT_QUALITY_VALUES
+ export const EXPORT_FORMAT_VALUES
+ export type ExportFormat = (typeof EXPORT_FORMAT_VALUES)[number]
+ export type ExportQuality = (typeof EXPORT_QUALITY_VALUES)[number]
+ export interface ExportOptions {
+ format: ExportFormat
+ quality: ExportQuality
+ fps?: number
+ includeAudio?: boolean
+ onProgress?: ({ progress }: { progress: number }) => void
+ onCancel?: () => boolean
+ }
+ export interface ExportResult {
+ success: boolean
+ buffer?: ArrayBuffer
+ error?: string
+ cancelled?: boolean
+ }
+
+keybinding.ts
+ export type ModifierKeys = | "ctrl"
+ | "alt"
+ | "shift"
+ | "ctrl+shift"
+ | "alt+shift"
+ | "ctrl+alt"
+ | "ctrl+alt+shift"
+ export type Key = | "a"
+ | "b"
+ | "c"
+ | "d"
+ | "e"
+ | "f"
+ | "g"
+ | "h"
+ | "i"
+ | "j"
+ | "k"
+ | "l"
+ | "m"
+ | "n"
+ ...
+ export type ModifierBasedShortcutKey = `${ModifierKeys}+${Key}`
+ export type SingleCharacterShortcutKey = `${Key}`
+ export type ShortcutKey = ModifierBasedShortcutKey | SingleCharacterShortcutKey
+ export type KeybindingConfig = {
+ [key in ShortcutKey]?: TActionWithOptionalArgs;
+ }
+
+language.ts
+ export type Language = (typeof LANGUAGES)[number]
+ export type LanguageCode = Language["code"]
+
+project.ts
+ export type TBackground = | {
+ type: "color";
+ color: string;
+ }
+ | {
+ type: "blur";
+ blurIntensity: number;
+ }
+ export interface TCanvasSize {
+ width: number
+ height: number
+ }
+ export interface TProjectMetadata {
+ id: string
+ name: string
+ thumbnail?: string
+ duration: number
+ createdAt: Date
+ updatedAt: Date
+ }
+ export interface TProjectSettings {
+ fps: number
+ canvasSize: TCanvasSize
+ originalCanvasSize?: TCanvasSize | null
+ background: TBackground
+ }
+ export interface TTimelineViewState {
+ zoomLevel: number
+ scrollLeft: number
+ playheadTime: number
+ }
+ export interface TProject {
+ metadata: TProjectMetadata
+ scenes: TScene[]
+ currentSceneId: string
+ settings: TProjectSettings
+ version: number
+ timelineViewState?: TTimelineViewState
+ }
+ export type TProjectSortKey = "createdAt" | "updatedAt" | "name" | "duration"
+ export type TSortOrder = "asc" | "desc"
+ export type TProjectSortOption = `${TProjectSortKey}-${TSortOrder}`
+
+sounds.ts
+ export interface SoundEffect {
+ id: number
+ name: string
+ description: string
+ url: string
+ previewUrl?: string
+ downloadUrl?: string
+ duration: number
+ filesize: number
+ type: string
+ channels: number
+ bitrate: number
+ bitdepth: number
+ samplerate: number
+ username: string
+ tags: string[]
+ license: string
+ created: string
+ downloads: number
+ rating: number
+ ratingCount: number
+ }
+ export interface SavedSound {
+ id: number
+ name: string
+ username: string
+ previewUrl?: string
+ downloadUrl?: string
+ duration: number
+ tags: string[]
+ license: string
+ savedAt: string
+ }
+ export interface SavedSoundsData {
+ sounds: SavedSound[]
+ lastModified: string
+ }
+
+stickers.ts
+ export type StickerCategory = (typeof STICKER_CATEGORIES)[number]
+
+time.ts
+ export type TTimeCode = "MM:SS" | "HH:MM:SS" | "HH:MM:SS:CS" | "HH:MM:SS:FF"
+
+timeline.ts
+ export interface TScene {
+ id: string
+ name: string
+ isMain: boolean
+ tracks: TimelineTrack[]
+ bookmarks: number[]
+ createdAt: Date
+ updatedAt: Date
+ }
+ export type TrackType = "video" | "text" | "audio" | "sticker"
+ export interface VideoTrack extends BaseTrack {
+ type: "video"
+ elements: (VideoElement | ImageElement)[]
+ isMain: boolean
+ muted: boolean
+ hidden: boolean
+ }
+ export interface TextTrack extends BaseTrack {
+ type: "text"
+ elements: TextElement[]
+ hidden: boolean
+ }
+ export interface AudioTrack extends BaseTrack {
+ type: "audio"
+ elements: AudioElement[]
+ muted: boolean
+ }
+ export interface StickerTrack extends BaseTrack {
+ type: "sticker"
+ elements: StickerElement[]
+ hidden: boolean
+ }
+ export type TimelineTrack = VideoTrack | TextTrack | AudioTrack | StickerTrack
+ export interface Transform {
+ scale: number
+ position: {
+ x: number;
+ y: number;
+ }
+ rotate: number
+ }
+ export interface UploadAudioElement extends BaseAudioElement {
+ sourceType: "upload"
+ mediaId: string
+ }
+ export interface LibraryAudioElement extends BaseAudioElement {
+ sourceType: "library"
+ sourceUrl: string
+ }
+ export type AudioElement = UploadAudioElement | LibraryAudioElement
+ export interface VideoElement extends BaseTimelineElement {
+ type: "video"
+ mediaId: string
+ muted?: boolean
+ hidden?: boolean
+ transform: Transform
+ opacity: number
+ }
+ export interface ImageElement extends BaseTimelineElement {
+ type: "image"
+ mediaId: string
+ hidden?: boolean
+ transform: Transform
+ opacity: number
+ }
+ export interface TextElement extends BaseTimelineElement {
+ type: "text"
+ content: string
+ fontSize: number
+ fontFamily: string
+ color: string
+ backgroundColor: string
+ textAlign: "left" | "center" | "right"
+ fontWeight: "normal" | "bold"
+ fontStyle: "normal" | "italic"
+ textDecoration: "none" | "underline" | "line-through"
+ hidden?: boolean
+ transform: Transform
+ opacity: number
+ }
+ export interface StickerElement extends BaseTimelineElement {
+ type: "sticker"
+ iconName: string
+ hidden?: boolean
+ transform: Transform
+ opacity: number
+ color?: string
+ }
+ export type TimelineElement = | AudioElement
+ | VideoElement
+ | ImageElement
+ | TextElement
+ | StickerElement
+ export type ElementType = TimelineElement["type"]
+ export type CreateUploadAudioElement = Omit<UploadAudioElement, "id">
+ export type CreateLibraryAudioElement = Omit<LibraryAudioElement, "id">
+ export type CreateAudioElement = | CreateUploadAudioElement
+ | CreateLibraryAudioElement
+ export type CreateVideoElement = Omit<VideoElement, "id">
+ export type CreateImageElement = Omit<ImageElement, "id">
+ export type CreateTextElement = Omit<TextElement, "id">
+ export type CreateStickerElement = Omit<StickerElement, "id">
+ export type CreateTimelineElement = | CreateAudioElement
+ | CreateVideoElement
+ | CreateImageElement
+ | CreateTextElement
+ | CreateSt...
+ export interface ElementDragState {
+ isDragging: boolean
+ elementId: string | null
+ trackId: string | null
+ startMouseX: number
+ startMouseY: number
+ startElementTime: number
+ clickOffsetTime: number
+ currentTime: number
+ currentMouseY: number
+ }
+ export interface DropTarget {
+ trackIndex: number
+ isNewTrack: boolean
+ insertPosition: "above" | "below" | null
+ xPosition: number
+ }
+ export interface ComputeDropTargetParams {
+ elementType: ElementType
+ mouseX: number
+ mouseY: number
+ tracks: TimelineTrack[]
+ playheadTime: number
+ isExternalDrop: boolean
+ elementDuration: number
+ pixelsPerSecond: number
+ zoomLevel: number
+ verticalDragDirection?: "up" | "down" | null
+ startTimeOverride?: number
+ excludeElementId?: string
+ }
+ export interface ClipboardItem {
+ trackId: string
+ trackType: TrackType
+ element: CreateTimelineElement
+ }
+
+transcription.ts
+ export type TranscriptionLanguage = LanguageCode | "auto"
+ export interface TranscriptionSegment {
+ text: string
+ start: number
+ end: number
+ }
+ export interface TranscriptionResult {
+ text: string
+ segments: TranscriptionSegment[]
+ language: string
+ }
+ export type TranscriptionStatus = | "idle"
+ | "loading-model"
+ | "transcribing"
+ | "complete"
+ | "error"
+ export interface TranscriptionProgress {
+ status: TranscriptionStatus
+ progress: number
+ message?: string
+ }
+ export type TranscriptionModelId = | "whisper-tiny"
+ | "whisper-small"
+ | "whisper-medium"
+ | "whisper-large-v3-turbo"
+ export interface TranscriptionModel {
+ id: TranscriptionModelId
+ name: string
+ huggingFaceId: string
+ description: string
+ }
+ export interface CaptionChunk {
+ text: string
+ startTime: number
+ duration: number
+ }
+
+## apps/web/src/utils
+
+browser.ts
+ export function isTypableDOMElement({
+ element,
+ }: {
+ element: HTMLElement;
+ }): boolean
+
+date.ts
+ export function formatDate({ date }: { date: Date }): string
+
+geometry.ts
+ export function dimensionToAspectRatio({
+ width,
+ height,
+ }: {
+ width: number;
+ height: number;
+ }): string
+
+id.ts
+ export function generateUUID(): string
+
+math.ts
+ export function clamp({
+ value,
+ min,
+ max,
+ }: {
+ value: number;
+ min: number;
+ max: number;
+ }): number
+
+platform.ts
+ export function getPlatformSpecialKey(): string
+ export function getPlatformAlternateKey(): string
+ export function isAppleDevice(): boolean
+
+string.ts
+ export function capitalizeFirstLetter({ string }: { string: string })
+ export function uppercase({ string }: { string: string })
+
+ui.ts
+ export function cn(...inputs: ClassValue[]): string
+
+## packages/ui/src/icons
+
+brand.tsx
+ export function OcVercelIcon({ className }: { className?: string })
+ export function OcMarbleIcon({
+ className = "",
+ size = 32,
+ }: {
+ className?: string;
+ size?: number;
+ })
+ export function OcDataBuddyIcon({
+ className = "",
+ size = 32,
+ }: {
+ className?: string;
+ size?: number;
+ })
+
+ui.tsx
+ export function OcVideoIcon({
+ className = "",
+ size = 32,
+ }: {
+ className?: string;
+ size?: number;
+ })
+
+```
+
+---
+
+*Generated and maintained by [Twiggy](https://github.com/twiggy-tools/Twiggy)*
diff --git a/.cursor/rules/comments.mdc b/.cursor/rules/comments.mdc
new file mode 100644
index 00000000..e732e859
--- /dev/null
+++ b/.cursor/rules/comments.mdc
@@ -0,0 +1,35 @@
+---
+alwaysApply: true
+---
+
+# Comment Guidelines
+
+## Good Comments (Human-style)
+- Explain WHY, not WHAT
+- Document non-obvious behavior or edge cases
+- Warn about performance implications or side effects
+- Explain business logic that isn't clear from code
+
+Examples:
+```javascript
+// transfer, not copy; sender buffer detaches
+// satisfies: check shape; keep literals
+// keep multibyte across chunks
+// timingSafeEqual throws on length mismatch
+```
+
+## Bad Comments (AI-style)
+- Don't explain what the code literally does
+- Don't add changelog-style comments in code
+- Don't comment every line or obvious operations
+
+Avoid:
+```javascript
+// Prevent duplicate initialization
+// Check if project is already loaded
+// Mark as initializing to prevent race conditions
+// (changed from blah to blah)
+```
+
+## Rule
+Only add comments when there's genuinely non-obvious behavior, performance considerations, or business logic that needs context. Code should be self-documenting through naming and structure. \ No newline at end of file
diff --git a/.cursor/rules/handling-uncertainty.mdc b/.cursor/rules/handling-uncertainty.mdc
new file mode 100644
index 00000000..c6bae730
--- /dev/null
+++ b/.cursor/rules/handling-uncertainty.mdc
@@ -0,0 +1,21 @@
+---
+alwaysApply: true
+---
+
+# Handling Uncertainty
+
+## Principle
+If you can't confidently respond due to missing context, data access, or ambiguity (multiple interpretations), report it instead of guessing. Seek clarification to avoid errors.
+
+Apply when: query lacks details, no access to info/tools, or unclear intent.
+
+## How to Report
+1. **Description**: Why uncertain and what you need.
+2. **Questions**: 1-3 targeted ones.
+3. **Assumptions** (opt.): State if proceeding; omit otherwise.
+
+Direct and concise.
+
+**Assumptions**: None.
+
+Builds transparency. \ No newline at end of file
diff --git a/.cursor/rules/readability.mdc b/.cursor/rules/readability.mdc
new file mode 100644
index 00000000..6be08cf7
--- /dev/null
+++ b/.cursor/rules/readability.mdc
@@ -0,0 +1,9 @@
+---
+alwaysApply: true
+---
+
+# Readability First
+
+Optimize code for AI agents to understand and modify.
+
+Never abbreviate. `event` not `e`, `element` not `el`. If it's easy to read, it's correct. In this case, "config" is better than "configuration" because it's shorter and is **still very readable**. "El" is not very readable.
diff --git a/.cursor/rules/separation-of-concerns.mdc b/.cursor/rules/separation-of-concerns.mdc
new file mode 100644
index 00000000..0dd97118
--- /dev/null
+++ b/.cursor/rules/separation-of-concerns.mdc
@@ -0,0 +1,52 @@
+---
+alwaysApply: true
+---
+
+# Separation of Concerns
+
+## Core Principle
+
+Each file should have one single purpose/responsibility. Related functionality should be grouped together, unrelated functionality should be separated.
+
+## Good Separation
+
+- One file per major concern (auth, validation, data transformation)
+- Group related utilities together
+- Extract shared logic into dedicated files
+- Keep API routes focused on their specific endpoint logic
+
+Examples:
+
+```javascript
+// ✅ Good: Each file has clear responsibility
+/lib/rate-limit.ts // Rate limiting utilities
+/lib/validation.ts // Input validation schemas
+/lib/freesound-api.ts // External API integration
+/api/sounds/search/route.ts // Route handler only
+```
+
+## Bad Mixing of Concerns
+
+Avoid cramming multiple responsibilities into one file:
+
+```javascript
+// ❌ Bad: Route file doing everything
+/api/sounds/search/route.ts
+- Rate limiting logic
+- Validation schemas
+- API transformation
+- External API calls
+- Response formatting
+- Error handling utilities
+```
+
+## When to Separate
+
+- File is getting long (>500 lines)
+- Multiple distinct responsibilities in one file
+- Logic could be reused elsewhere
+- Complex utilities that distract from main purpose
+
+## Rule
+
+One file, one responsibility. Extract shared concerns into focused utility files \ No newline at end of file
diff --git a/.cursor/rules/ultracite.mdc b/.cursor/rules/ultracite.mdc
index 4be535af..977f4363 100644
--- a/.cursor/rules/ultracite.mdc
+++ b/.cursor/rules/ultracite.mdc
@@ -6,7 +6,7 @@ alwaysApply: true
# Project Context
-Ultracite enforces strict type safety, accessibility standards, and consistent code quality for JavaScript/TypeScript projects using Biome's lightning-fast formatter and linter.
+Ultracite enforces strict type safety, accessibility standards, and consistent code quality for JavaScript/TypeScript projects using Biome's formatter.
## Key Principles
@@ -43,7 +43,6 @@ Ultracite enforces strict type safety, accessibility standards, and consistent c
### React and JSX Best Practices
- Don't import `React` itself.
-- Don't define React components inside other components.
- Don't use both `children` and `dangerouslySetInnerHTML` props on the same element.
- Don't insert comments as text nodes.
- Use `<>...</>` instead of `<Fragment>...</Fragment>`.
diff --git a/.cursor/rules/writing-scannable-code.mdc b/.cursor/rules/writing-scannable-code.mdc
new file mode 100644
index 00000000..97ed87e9
--- /dev/null
+++ b/.cursor/rules/writing-scannable-code.mdc
@@ -0,0 +1,53 @@
+---
+alwaysApply: true
+---
+
+# Scannable Code Guidelines/Separating Concerns.
+
+## Core Principle
+
+Code should be scannable through proper abstraction, not comments. Use variables and helper functions to make intent clear at a glance.
+
+## Good Scannable Code
+
+- Extract complex logic into well-named variables
+- Create helper functions for multi-step operations
+- Use descriptive names that explain intent
+
+Examples:
+
+```javascript
+// ✅ Scannable: Intent is clear from variable names
+const isValidUser = user.isActive && user.hasPermissions;
+const shouldProcessPayment = amount > 0 && !order.isPaid;
+
+// ✅ Scannable: Complex logic extracted to helper
+const searchParams = buildFreesoundSearchParams({ query, filters, pagination });
+const transformedResults = transformFreesoundResults({ rawResults });
+```
+
+## Bad Unscannable Code
+
+Avoid:
+
+```javascript
+// ❌ Hard to scan: What does this condition mean?
+if (type === "effects" || !type) {
+ params.append("filter", "duration:[* TO 30.0]");
+ params.append("filter", `avg_rating:[${min_rating} TO *]`);
+ if (commercial_only) {
+ params.append("filter", 'license:("Attribution" OR "Creative Commons 0")');
+ }
+}
+
+// ❌ Hard to scan: Complex ternary
+const sortParam = query
+ ? sort === "score"
+ ? "score"
+ : `${sort}_desc`
+ : `${sort}_desc`;
+```
+
+## Rule
+
+Make code scannable by extracting intent into variables and helper functions. If you need to think about what code does, extract it. The reader should understand the flow without diving into implementation details. \ No newline at end of file