summaryrefslogtreecommitdiff
path: root/apps/web/src/components/editor/panels/assets/views
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-20 11:31:17 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-20 11:31:17 +0200
commit3e89d2998588f8e35c524e523ba2ee853f0f6475 (patch)
tree57221f96c1259b3b07e3ddf8c1adefc502b9d8eb /apps/web/src/components/editor/panels/assets/views
parent729d10592f4e802f91c555c309ff3ba4b119f843 (diff)
downloadopencut-3e89d2998588f8e35c524e523ba2ee853f0f6475.tar.gz
refactor: restructure files to their domains, new preview overlay system, and dep graph
Diffstat (limited to 'apps/web/src/components/editor/panels/assets/views')
-rw-r--r--apps/web/src/components/editor/panels/assets/views/assets.tsx24
-rw-r--r--apps/web/src/components/editor/panels/assets/views/captions.tsx337
-rw-r--r--apps/web/src/components/editor/panels/assets/views/effects.tsx92
-rw-r--r--apps/web/src/components/editor/panels/assets/views/settings/background.tsx6
-rw-r--r--apps/web/src/components/editor/panels/assets/views/settings/index.tsx10
-rw-r--r--apps/web/src/components/editor/panels/assets/views/sounds.tsx563
-rw-r--r--apps/web/src/components/editor/panels/assets/views/stickers.tsx455
-rw-r--r--apps/web/src/components/editor/panels/assets/views/text.tsx46
8 files changed, 20 insertions, 1513 deletions
diff --git a/apps/web/src/components/editor/panels/assets/views/assets.tsx b/apps/web/src/components/editor/panels/assets/views/assets.tsx
index 33b47c67..4afd22b7 100644
--- a/apps/web/src/components/editor/panels/assets/views/assets.tsx
+++ b/apps/web/src/components/editor/panels/assets/views/assets.tsx
@@ -25,28 +25,28 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
-import { DEFAULT_NEW_ELEMENT_DURATION } from "@/lib/timeline/creation";
-import { TICKS_PER_SECOND } from "@/lib/wasm";
-import { useEditor } from "@/hooks/use-editor";
-import { useFileUpload } from "@/hooks/use-file-upload";
-import { invokeAction } from "@/lib/actions";
-import { processMediaAssets } from "@/lib/media/processing";
-import { showMediaUploadToast } from "@/lib/media/upload-toast";
+import { DEFAULT_NEW_ELEMENT_DURATION } from "@/timeline/creation";
+import { TICKS_PER_SECOND } from "@/wasm";
+import { useEditor } from "@/editor/use-editor";
+import { useFileUpload } from "@/media/use-file-upload";
+import { invokeAction } from "@/actions";
+import { processMediaAssets } from "@/media/processing";
+import { showMediaUploadToast } from "@/media/upload-toast";
import {
SelectableItem,
SelectableSurface,
useSelection,
useSelectionScope,
-} from "@/lib/selection";
-import { buildElementFromMedia } from "@/lib/timeline/element-utils";
+} from "@/selection";
+import { buildElementFromMedia } from "@/timeline/element-utils";
import {
type MediaSortKey,
type MediaSortOrder,
type MediaViewMode,
useAssetsPanelStore,
-} from "@/stores/assets-panel-store";
-import { MASKABLE_ELEMENT_TYPES } from "@/lib/timeline";
-import type { MediaAsset } from "@/lib/media/types";
+} from "@/components/editor/panels/assets/assets-panel-store";
+import { MASKABLE_ELEMENT_TYPES } from "@/timeline";
+import type { MediaAsset } from "@/media/types";
import { cn } from "@/utils/ui";
import {
CloudUploadIcon,
diff --git a/apps/web/src/components/editor/panels/assets/views/captions.tsx b/apps/web/src/components/editor/panels/assets/views/captions.tsx
deleted file mode 100644
index 040df749..00000000
--- a/apps/web/src/components/editor/panels/assets/views/captions.tsx
+++ /dev/null
@@ -1,337 +0,0 @@
-import { Button } from "@/components/ui/button";
-import { PanelView } from "@/components/editor/panels/assets/views/base-panel";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import { useReducer, useRef, useState } from "react";
-import { extractTimelineAudio } from "@/lib/media/mediabunny";
-import { useEditor } from "@/hooks/use-editor";
-import { TRANSCRIPTION_DIAGNOSTICS_SCOPE } from "@/lib/transcription/diagnostics";
-import { DEFAULT_TRANSCRIPTION_SAMPLE_RATE } from "@/lib/transcription/audio";
-import { TRANSCRIPTION_LANGUAGES } from "@/lib/transcription/supported-languages";
-import type {
- CaptionChunk,
- TranscriptionLanguage,
- TranscriptionProgress,
-} from "@/lib/transcription/types";
-import { transcriptionService } from "@/services/transcription/service";
-import { decodeAudioToFloat32 } from "@/lib/media/audio";
-import { buildCaptionChunks } from "@/lib/transcription/caption";
-import { insertCaptionChunksAsTextTrack } from "@/lib/subtitles/insert";
-import { parseSubtitleFile } from "@/lib/subtitles/parse";
-import { Spinner } from "@/components/ui/spinner";
-import {
- Section,
- SectionContent,
- SectionField,
- SectionFields,
-} from "@/components/section";
-import { AlertCircleIcon, CloudUploadIcon } from "@hugeicons/core-free-icons";
-import { HugeiconsIcon } from "@hugeicons/react";
-import {
- Tooltip,
- TooltipContent,
- TooltipProvider,
- TooltipTrigger,
-} from "@/components/ui/tooltip";
-import type { DiagnosticSeverity } from "@/lib/diagnostics/types";
-
-const DIAGNOSTIC_BUTTON_VARIANT: Record<
- DiagnosticSeverity,
- "caution" | "destructive-foreground"
-> = {
- caution: "caution",
- error: "destructive-foreground",
-};
-
-type ProcessingState =
- | { status: "idle"; error: string | null; warnings: string[] }
- | { status: "processing"; step: string };
-
-type ProcessingAction =
- | { type: "start"; step: string }
- | { type: "update_step"; step: string }
- | { type: "succeed"; warnings: string[] }
- | { type: "fail"; error: string };
-
-const IDLE_STATE: ProcessingState = {
- status: "idle",
- error: null,
- warnings: [],
-};
-
-function processingReducer(
- state: ProcessingState,
- action: ProcessingAction,
-): ProcessingState {
- switch (action.type) {
- case "start":
- return { status: "processing", step: action.step };
- case "update_step":
- if (state.status !== "processing") return state;
- return { status: "processing", step: action.step };
- case "succeed":
- return { status: "idle", error: null, warnings: action.warnings };
- case "fail":
- return { status: "idle", error: action.error, warnings: [] };
- }
-}
-
-export function Captions() {
- const [selectedLanguage, setSelectedLanguage] =
- useState<TranscriptionLanguage>("auto");
- const [processing, dispatch] = useReducer(processingReducer, IDLE_STATE);
- const containerRef = useRef<HTMLDivElement>(null);
- const fileInputRef = useRef<HTMLInputElement>(null);
- const editor = useEditor();
-
- const isProcessing = processing.status === "processing";
-
- const activeDiagnostics = useEditor((e) =>
- e.diagnostics.getActive({ scope: TRANSCRIPTION_DIAGNOSTICS_SCOPE }),
- );
-
- const handleProgress = (progress: TranscriptionProgress) => {
- if (progress.status === "loading-model") {
- dispatch({
- type: "update_step",
- step: `Loading model ${Math.round(progress.progress)}%`,
- });
- } else if (progress.status === "transcribing") {
- dispatch({ type: "update_step", step: "Transcribing..." });
- }
- };
-
- const insertCaptions = ({
- captions,
- }: {
- captions: CaptionChunk[];
- }): boolean => {
- const trackId = insertCaptionChunksAsTextTrack({ editor, captions });
- return trackId !== null;
- };
-
- const handleGenerateTranscript = async () => {
- dispatch({ type: "start", step: "Extracting audio..." });
- try {
- const audioBlob = await extractTimelineAudio({
- tracks: editor.scenes.getActiveScene().tracks,
- mediaAssets: editor.media.getAssets(),
- totalDuration: editor.timeline.getTotalDuration(),
- });
-
- dispatch({ type: "update_step", step: "Preparing audio..." });
- const { samples } = await decodeAudioToFloat32({
- audioBlob,
- sampleRate: DEFAULT_TRANSCRIPTION_SAMPLE_RATE,
- });
-
- const result = await transcriptionService.transcribe({
- audioData: samples,
- language: selectedLanguage === "auto" ? undefined : selectedLanguage,
- onProgress: handleProgress,
- });
-
- dispatch({ type: "update_step", step: "Generating captions..." });
- const captionChunks = buildCaptionChunks({ segments: result.segments });
-
- if (!insertCaptions({ captions: captionChunks })) {
- dispatch({ type: "fail", error: "No captions were generated" });
- return;
- }
-
- dispatch({ type: "succeed", warnings: [] });
- } catch (error) {
- console.error("Transcription failed:", error);
- dispatch({
- type: "fail",
- error:
- error instanceof Error
- ? error.message
- : "An unexpected error occurred",
- });
- }
- };
-
- const handleImportClick = () => {
- fileInputRef.current?.click();
- };
-
- const handleImportFile = async ({ file }: { file: File }) => {
- dispatch({ type: "start", step: "Reading subtitle file..." });
- try {
- const input = await file.text();
- const result = parseSubtitleFile({
- fileName: file.name,
- input,
- });
-
- if (result.captions.length === 0) {
- dispatch({
- type: "fail",
- error: "No valid subtitle cues were found in the subtitle file",
- });
- return;
- }
-
- dispatch({ type: "update_step", step: "Importing subtitles..." });
-
- if (!insertCaptions({ captions: result.captions })) {
- dispatch({ type: "fail", error: "No captions were generated" });
- return;
- }
-
- const nextWarnings = [...result.warnings];
- if (result.skippedCueCount > 0) {
- nextWarnings.unshift(
- `Imported ${result.captions.length} subtitle cue(s) and skipped ${result.skippedCueCount} malformed cue(s).`,
- );
- }
-
- dispatch({ type: "succeed", warnings: nextWarnings });
- } catch (error) {
- console.error("Subtitle import failed:", error);
- dispatch({
- type: "fail",
- error:
- error instanceof Error
- ? error.message
- : "An unexpected error occurred",
- });
- }
- };
-
- const handleFileChange = async ({
- event,
- }: {
- event: React.ChangeEvent<HTMLInputElement>;
- }) => {
- const file = event.target.files?.[0];
- if (event.target) {
- event.target.value = "";
- }
- if (!file) return;
-
- await handleImportFile({ file });
- };
-
- const handleLanguageChange = ({ value }: { value: string }) => {
- if (value === "auto") {
- setSelectedLanguage("auto");
- return;
- }
-
- const matchedLanguage = TRANSCRIPTION_LANGUAGES.find(
- (language) => language.code === value,
- );
- if (!matchedLanguage) return;
- setSelectedLanguage(matchedLanguage.code);
- };
-
- const error = processing.status === "idle" ? processing.error : null;
- const warnings = processing.status === "idle" ? processing.warnings : [];
-
- return (
- <PanelView
- title="Captions"
- contentClassName="px-0 flex flex-col h-full"
- actions={
- <TooltipProvider>
- <div className="flex items-center gap-1.5">
- {!isProcessing &&
- activeDiagnostics.map((diagnostic) => (
- <Tooltip key={diagnostic.id}>
- <TooltipTrigger asChild>
- <Button
- variant={DIAGNOSTIC_BUTTON_VARIANT[diagnostic.severity]}
- size="icon"
- aria-label={diagnostic.message}
- >
- <HugeiconsIcon icon={AlertCircleIcon} size={16} />
- </Button>
- </TooltipTrigger>
- <TooltipContent>{diagnostic.message}</TooltipContent>
- </Tooltip>
- ))}
- <Button
- type="button"
- variant="outline"
- size="sm"
- onClick={handleImportClick}
- disabled={isProcessing}
- className="items-center justify-center gap-1.5"
- >
- <HugeiconsIcon icon={CloudUploadIcon} />
- Import
- </Button>
- </div>
- </TooltipProvider>
- }
- ref={containerRef}
- >
- <input
- ref={fileInputRef}
- type="file"
- accept=".srt,.ass"
- className="hidden"
- onChange={(event) => void handleFileChange({ event })}
- />
- <Section
- showTopBorder={false}
- showBottomBorder={false}
- className="flex-1"
- >
- <SectionContent className="flex flex-col gap-4 h-full pt-1">
- <SectionFields>
- <SectionField label="Language">
- <Select
- value={selectedLanguage}
- onValueChange={(value) => handleLanguageChange({ value })}
- >
- <SelectTrigger>
- <SelectValue placeholder="Select a language" />
- </SelectTrigger>
- <SelectContent>
- <SelectItem value="auto">Auto detect</SelectItem>
- {TRANSCRIPTION_LANGUAGES.map((language) => (
- <SelectItem key={language.code} value={language.code}>
- {language.name}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- </SectionField>
- </SectionFields>
-
- <Button
- type="button"
- className="mt-auto w-full"
- onClick={handleGenerateTranscript}
- disabled={isProcessing || activeDiagnostics.length > 0}
- >
- {isProcessing && <Spinner className="mr-1" />}
- {isProcessing ? processing.step : "Generate transcript"}
- </Button>
- {error && (
- <div className="bg-destructive/10 border-destructive/20 rounded-md border p-3">
- <p className="text-destructive text-sm">{error}</p>
- </div>
- )}
- {warnings.length > 0 && (
- <div className="rounded-md border border-amber-500/20 bg-amber-500/10 p-3">
- <ul className="space-y-1 text-sm text-amber-700">
- {warnings.map((warning) => (
- <li key={warning}>{warning}</li>
- ))}
- </ul>
- </div>
- )}
- </SectionContent>
- </Section>
- </PanelView>
- );
-}
diff --git a/apps/web/src/components/editor/panels/assets/views/effects.tsx b/apps/web/src/components/editor/panels/assets/views/effects.tsx
deleted file mode 100644
index 74128df6..00000000
--- a/apps/web/src/components/editor/panels/assets/views/effects.tsx
+++ /dev/null
@@ -1,92 +0,0 @@
-"use client";
-
-import { useEffect, useRef, useCallback } from "react";
-import { PanelView } from "@/components/editor/panels/assets/views/base-panel";
-import { DraggableItem } from "@/components/editor/panels/assets/draggable-item";
-import { effectsRegistry, EFFECT_TARGET_ELEMENT_TYPES } from "@/lib/effects";
-import { effectPreviewService } from "@/services/renderer/effect-preview";
-import { useEditor } from "@/hooks/use-editor";
-import { buildEffectElement } from "@/lib/timeline/element-utils";
-import type { EffectDefinition } from "@/lib/effects/types";
-
-export function EffectsView() {
- const effects = effectsRegistry.getAll();
-
- return (
- <PanelView title="Effects">
- <EffectsGrid effects={effects} />
- </PanelView>
- );
-}
-
-function EffectsGrid({ effects }: { effects: EffectDefinition[] }) {
- return (
- <div
- className="grid gap-2"
- style={{ gridTemplateColumns: "repeat(auto-fill, minmax(96px, 1fr))" }}
- >
- {effects.map((effect) => (
- <EffectItem key={effect.type} effect={effect} />
- ))}
- </div>
- );
-}
-
-function EffectPreviewCanvas({ effectType }: { effectType: string }) {
- const canvasRef = useRef<HTMLCanvasElement>(null);
-
- useEffect(() => {
- const render = () => {
- if (canvasRef.current) {
- effectPreviewService.renderPreview({
- effectType,
- params: {},
- targetCanvas: canvasRef.current,
- });
- }
- };
-
- render();
- return effectPreviewService.onPreviewImageReady({ callback: render });
- }, [effectType]);
-
- return <canvas ref={canvasRef} className="size-full" />;
-}
-
-function EffectItem({ effect }: { effect: EffectDefinition }) {
- const editor = useEditor();
-
- const handleAddToTimeline = useCallback(() => {
- const currentTime = editor.playback.getCurrentTime();
- const element = buildEffectElement({
- effectType: effect.type,
- startTime: currentTime,
- });
-
- editor.timeline.insertElement({
- placement: { mode: "auto", trackType: "effect" },
- element,
- });
- }, [editor, effect.type]);
-
- const preview = <EffectPreviewCanvas effectType={effect.type} />;
-
- return (
- <DraggableItem
- name={effect.name}
- preview={preview}
- dragData={{
- id: effect.type,
- name: effect.name,
- type: "effect",
- effectType: effect.type,
- targetElementTypes: EFFECT_TARGET_ELEMENT_TYPES,
- }}
- onAddToTimeline={handleAddToTimeline}
- aspectRatio={1}
- isRounded
- variant="card"
- containerClassName="w-full"
- />
- );
-}
diff --git a/apps/web/src/components/editor/panels/assets/views/settings/background.tsx b/apps/web/src/components/editor/panels/assets/views/settings/background.tsx
index 099e335a..9ce34ebb 100644
--- a/apps/web/src/components/editor/panels/assets/views/settings/background.tsx
+++ b/apps/web/src/components/editor/panels/assets/views/settings/background.tsx
@@ -12,12 +12,12 @@ import { Popover, PopoverTrigger } from "@/components/ui/popover";
import {
BACKGROUND_BLUR_INTENSITY_PRESETS,
DEFAULT_BACKGROUND_BLUR_INTENSITY,
-} from "@/lib/background/blur";
-import { DEFAULT_BACKGROUND_COLOR } from "@/lib/background/color";
+} from "@/background/blur";
+import { DEFAULT_BACKGROUND_COLOR } from "@/background/color";
import { patternCraftGradients } from "@/data/colors/pattern-craft";
import { colors } from "@/data/colors/solid";
import { syntaxUIGradients } from "@/data/colors/syntax-ui";
-import { useEditor } from "@/hooks/use-editor";
+import { useEditor } from "@/editor/use-editor";
import { effectPreviewService } from "@/services/renderer/effect-preview";
import { cn } from "@/utils/ui";
diff --git a/apps/web/src/components/editor/panels/assets/views/settings/index.tsx b/apps/web/src/components/editor/panels/assets/views/settings/index.tsx
index 4d87493a..bf3a1c16 100644
--- a/apps/web/src/components/editor/panels/assets/views/settings/index.tsx
+++ b/apps/web/src/components/editor/panels/assets/views/settings/index.tsx
@@ -9,9 +9,9 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
-import { FPS_PRESETS } from "@/lib/fps/presets";
-import { floatToFrameRate, frameRateToFloat } from "@/lib/fps/utils";
-import { useEditor } from "@/hooks/use-editor";
+import { FPS_PRESETS } from "@/fps/presets";
+import { floatToFrameRate, frameRateToFloat } from "@/fps/utils";
+import { useEditor } from "@/editor/use-editor";
import {
Section,
SectionContent,
@@ -22,7 +22,7 @@ import { BackgroundContent } from "./background";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import { NumberField } from "@/components/ui/number-field";
-import { useEditorStore } from "@/stores/editor-store";
+import { useEditorStore } from "@/editor/editor-store";
import { usePropertyDraft } from "@/components/editor/panels/properties/hooks/use-property-draft";
import { HugeiconsIcon } from "@hugeicons/react";
import { Tick02Icon } from "@hugeicons/core-free-icons";
@@ -30,7 +30,7 @@ import { cn } from "@/utils/ui";
import { dimensionToAspectRatio } from "@/utils/geometry";
import { formatNumberForDisplay } from "@/utils/math";
import { OcSquarePlusIcon } from "@/components/icons";
-import type { TCanvasSize } from "@/lib/project/types";
+import type { TCanvasSize } from "@/project/types";
type SettingsView = "project-info" | "background";
diff --git a/apps/web/src/components/editor/panels/assets/views/sounds.tsx b/apps/web/src/components/editor/panels/assets/views/sounds.tsx
deleted file mode 100644
index c7a1029c..00000000
--- a/apps/web/src/components/editor/panels/assets/views/sounds.tsx
+++ /dev/null
@@ -1,563 +0,0 @@
-"use client";
-
-import { useEffect, useState } from "react";
-import { Button } from "@/components/ui/button";
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog";
-import {
- DropdownMenu,
- DropdownMenuCheckboxItem,
- DropdownMenuContent,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import { Input } from "@/components/ui/input";
-import { ScrollArea } from "@/components/ui/scroll-area";
-import { Separator } from "@/components/ui/separator";
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
-import { useInfiniteScroll } from "@/hooks/use-infinite-scroll";
-import { useSoundSearch } from "@/hooks/use-sound-search";
-import { useSoundsStore } from "@/stores/sounds-store";
-import type { SavedSound, SoundEffect } from "@/lib/sounds/types";
-import { cn } from "@/utils/ui";
-import {
- FavouriteIcon,
- FilterMailIcon,
- PauseIcon,
- PlayIcon,
- PlusSignIcon,
-} from "@hugeicons/core-free-icons";
-import { HugeiconsIcon } from "@hugeicons/react";
-
-export function SoundsView() {
- return (
- <div className="flex h-full flex-col">
- <Tabs defaultValue="sound-effects" className="flex h-full flex-col">
- <div className="px-3 pt-4 pb-0">
- <TabsList>
- <TabsTrigger value="sound-effects">Sound effects</TabsTrigger>
- <TabsTrigger value="saved">Saved</TabsTrigger>
- </TabsList>
- </div>
- <Separator className="my-4" />
- <TabsContent
- value="sound-effects"
- className="mt-0 flex min-h-0 flex-1 flex-col p-5 pt-0"
- >
- <SoundEffectsView />
- </TabsContent>
- <TabsContent
- value="saved"
- className="mt-0 flex min-h-0 flex-1 flex-col p-5 pt-0"
- >
- <SavedSoundsView />
- </TabsContent>
- </Tabs>
- </div>
- );
-}
-
-function SoundEffectsView() {
- const {
- topSoundEffects,
- isLoading,
- searchQuery,
- setSearchQuery,
- scrollPosition,
- setScrollPosition,
- loadSavedSounds,
- showCommercialOnly,
- toggleCommercialFilter,
- hasLoaded,
- setTopSoundEffects,
- setLoading,
- setError,
- setHasLoaded,
- setCurrentPage,
- setHasNextPage,
- setTotalCount,
- } = useSoundsStore();
- const {
- results: searchResults,
- isLoading: isSearching,
- loadMore,
- hasNextPage,
- isLoadingMore,
- } = useSoundSearch({
- query: searchQuery,
- commercialOnly: showCommercialOnly,
- });
-
- const [playingId, setPlayingId] = useState<number | null>(null);
- const [audioElement, setAudioElement] = useState<HTMLAudioElement | null>(
- null,
- );
-
- const { scrollAreaRef, handleScroll } = useInfiniteScroll({
- onLoadMore: loadMore,
- hasMore: hasNextPage,
- isLoading: isLoadingMore || isSearching,
- });
-
- useEffect(() => {
- loadSavedSounds();
- }, [loadSavedSounds]);
-
- useEffect(() => {
- if (hasLoaded) {
- return;
- }
-
- let shouldIgnore = false;
-
- const fetchTopSounds = async () => {
- try {
- if (!shouldIgnore) {
- setLoading({ loading: true });
- setError({ error: null });
- }
-
- const response = await fetch(
- "/api/sounds/search?page_size=50&sort=downloads",
- );
-
- if (!shouldIgnore) {
- if (!response.ok) {
- throw new Error(`Failed to fetch: ${response.status}`);
- }
-
- const data = await response.json();
- setTopSoundEffects({ sounds: data.results });
- setHasLoaded({ loaded: true });
-
- setCurrentPage({ page: 1 });
- setHasNextPage({ hasNext: !!data.next });
- setTotalCount({ count: data.count });
- }
- } catch (error) {
- if (!shouldIgnore) {
- console.error("Failed to fetch top sounds:", error);
- setError({
- error:
- error instanceof Error ? error.message : "Failed to load sounds",
- });
- }
- } finally {
- if (!shouldIgnore) {
- setLoading({ loading: false });
- }
- }
- };
-
- const timeoutId = setTimeout(fetchTopSounds, 100, {});
-
- return () => {
- shouldIgnore = true;
- clearTimeout(timeoutId);
- };
- }, [
- hasLoaded,
- setTopSoundEffects,
- setLoading,
- setError,
- setHasLoaded,
- setCurrentPage,
- setHasNextPage,
- setTotalCount,
- ]);
-
- useEffect(() => {
- if (!scrollAreaRef.current || scrollPosition <= 0) {
- return;
- }
-
- const restoreScrollPosition = () => {
- scrollAreaRef.current?.scrollTo({ top: scrollPosition });
- };
-
- const timeoutId = setTimeout(restoreScrollPosition, 100, {});
-
- return () => clearTimeout(timeoutId);
- }, [scrollPosition, scrollAreaRef]);
-
- const handleScrollWithPosition = ({
- currentTarget,
- }: React.UIEvent<HTMLDivElement>) => {
- const { scrollTop } = currentTarget;
- setScrollPosition({ position: scrollTop });
- handleScroll({ currentTarget } as React.UIEvent<HTMLDivElement>);
- };
-
- const displayedSounds = searchQuery ? searchResults : topSoundEffects;
-
- const playSound = ({ sound }: { sound: SoundEffect }) => {
- if (playingId === sound.id) {
- audioElement?.pause();
- setPlayingId(null);
- return;
- }
-
- audioElement?.pause();
-
- if (sound.previewUrl) {
- const audio = new Audio(sound.previewUrl);
- audio.addEventListener("ended", () => {
- setPlayingId(null);
- });
- audio.addEventListener("error", () => {
- setPlayingId(null);
- });
- audio.play().catch((error) => {
- console.error("Failed to play sound preview:", error);
- setPlayingId(null);
- });
-
- setAudioElement(audio);
- setPlayingId(sound.id);
- }
- };
-
- return (
- <div className="mt-1 flex h-full flex-col gap-5">
- <div className="flex items-center gap-3">
- <Input
- placeholder="Search sound effects"
- className="w-full"
- containerClassName="w-full"
- value={searchQuery}
- onChange={({ currentTarget }) =>
- setSearchQuery({ query: currentTarget.value })
- }
- showClearIcon
- onClear={() => setSearchQuery({ query: "" })}
- />
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button
- variant="text"
- size="icon"
- className={cn(showCommercialOnly && "text-primary")}
- >
- <HugeiconsIcon icon={FilterMailIcon} />
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent align="end" className="w-56">
- <DropdownMenuCheckboxItem
- checked={showCommercialOnly}
- onCheckedChange={() => toggleCommercialFilter()}
- >
- Show only commercially licensed
- </DropdownMenuCheckboxItem>
- <div className="text-muted-foreground px-2 py-1.5 text-xs">
- {showCommercialOnly
- ? "Only showing sounds licensed for commercial use"
- : "Showing all sounds regardless of license"}
- </div>
- </DropdownMenuContent>
- </DropdownMenu>
- </div>
-
- <div className="relative h-full overflow-hidden">
- <ScrollArea
- className="h-full flex-1"
- ref={scrollAreaRef}
- onScrollCapture={handleScrollWithPosition}
- >
- <div className="flex flex-col gap-4">
- {isLoading && !searchQuery && (
- <div className="text-muted-foreground text-sm">
- Loading sounds...
- </div>
- )}
- {isSearching && searchQuery && (
- <div className="text-muted-foreground text-sm">Searching...</div>
- )}
- {displayedSounds.map((sound) => (
- <AudioItem
- key={sound.id}
- sound={sound}
- isPlaying={playingId === sound.id}
- onPlay={playSound}
- />
- ))}
- {!isLoading && !isSearching && displayedSounds.length === 0 && (
- <div className="text-muted-foreground text-sm">
- {searchQuery ? "No sounds found" : "No sounds available"}
- </div>
- )}
- {isLoadingMore && (
- <div className="text-muted-foreground py-4 text-center text-sm">
- Loading more sounds...
- </div>
- )}
- </div>
- </ScrollArea>
- </div>
- </div>
- );
-}
-
-function SavedSoundsView() {
- const {
- savedSounds,
- isLoadingSavedSounds,
- savedSoundsError,
- loadSavedSounds,
- clearSavedSounds,
- } = useSoundsStore();
-
- const [playingId, setPlayingId] = useState<number | null>(null);
- const [audioElement, setAudioElement] = useState<HTMLAudioElement | null>(
- null,
- );
-
- const [showClearDialog, setShowClearDialog] = useState(false);
-
- useEffect(() => {
- loadSavedSounds();
- }, [loadSavedSounds]);
-
- const playSound = ({ sound }: { sound: SoundEffect }) => {
- if (playingId === sound.id) {
- audioElement?.pause();
- setPlayingId(null);
- return;
- }
-
- audioElement?.pause();
-
- if (sound.previewUrl) {
- const audio = new Audio(sound.previewUrl);
- audio.addEventListener("ended", () => {
- setPlayingId(null);
- });
- audio.addEventListener("error", () => {
- setPlayingId(null);
- });
- audio.play().catch((error) => {
- console.error("Failed to play sound preview:", error);
- setPlayingId(null);
- });
-
- setAudioElement(audio);
- setPlayingId(sound.id);
- }
- };
-
- const convertToSoundEffect = ({
- savedSound,
- }: {
- savedSound: SavedSound;
- }): SoundEffect => ({
- id: savedSound.id,
- name: savedSound.name,
- description: "",
- url: "",
- previewUrl: savedSound.previewUrl,
- downloadUrl: savedSound.downloadUrl,
- duration: savedSound.duration,
- filesize: 0,
- type: "audio",
- channels: 0,
- bitrate: 0,
- bitdepth: 0,
- samplerate: 0,
- username: savedSound.username,
- tags: savedSound.tags,
- license: savedSound.license,
- created: savedSound.savedAt,
- downloads: 0,
- rating: 0,
- ratingCount: 0,
- });
-
- if (isLoadingSavedSounds) {
- return (
- <div className="flex h-full items-center justify-center">
- <div className="text-muted-foreground text-sm">
- Loading saved sounds...
- </div>
- </div>
- );
- }
-
- if (savedSoundsError) {
- return (
- <div className="flex h-full items-center justify-center">
- <div className="text-destructive text-sm">
- Error: {savedSoundsError}
- </div>
- </div>
- );
- }
-
- if (savedSounds.length === 0) {
- return (
- <div className="bg-background flex h-full flex-col items-center justify-center gap-3 p-4">
- <HugeiconsIcon
- icon={FavouriteIcon}
- className="text-muted-foreground size-10"
- />
- <div className="flex flex-col gap-2 text-center">
- <p className="text-lg font-medium">No saved sounds</p>
- <p className="text-muted-foreground text-sm text-balance">
- Click the heart icon on any sound to save it here
- </p>
- </div>
- </div>
- );
- }
-
- return (
- <div className="mt-1 flex h-full flex-col gap-5">
- <div className="flex items-center justify-between">
- <p className="text-muted-foreground text-sm">
- {savedSounds.length} saved{" "}
- {savedSounds.length === 1 ? "sound" : "sounds"}
- </p>
- <Dialog open={showClearDialog} onOpenChange={setShowClearDialog}>
- <DialogTrigger asChild>
- <Button
- variant="text"
- size="sm"
- className="text-muted-foreground hover:text-destructive h-auto !opacity-100"
- >
- Clear all
- </Button>
- </DialogTrigger>
- <DialogContent>
- <DialogHeader>
- <DialogTitle>Clear all saved sounds?</DialogTitle>
- <DialogDescription>
- This will permanently remove all {savedSounds.length} saved
- sounds from your collection. This action cannot be undone.
- </DialogDescription>
- </DialogHeader>
- <DialogFooter>
- <Button variant="text" onClick={() => setShowClearDialog(false)}>
- Cancel
- </Button>
- <Button
- variant="destructive"
- onClick={async ({
- stopPropagation,
- }: React.MouseEvent<HTMLButtonElement>) => {
- stopPropagation();
- await clearSavedSounds();
- setShowClearDialog(false);
- }}
- >
- Clear all sounds
- </Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- </div>
-
- <div className="relative h-full overflow-hidden">
- <ScrollArea className="h-full flex-1">
- <div className="flex flex-col gap-4">
- {savedSounds.map((sound) => (
- <AudioItem
- key={sound.id}
- sound={convertToSoundEffect({ savedSound: sound })}
- isPlaying={playingId === sound.id}
- onPlay={playSound}
- />
- ))}
- </div>
- </ScrollArea>
- </div>
- </div>
- );
-}
-
-interface AudioItemProps {
- sound: SoundEffect;
- isPlaying: boolean;
- onPlay: ({ sound }: { sound: SoundEffect }) => void;
-}
-
-function AudioItem({ sound, isPlaying, onPlay }: AudioItemProps) {
- const { addSoundToTimeline, isSoundSaved, toggleSavedSound } =
- useSoundsStore();
- const isSaved = isSoundSaved({ soundId: sound.id });
-
- const handleClick = () => {
- onPlay({ sound });
- };
-
- const handleSaveClick = ({
- stopPropagation,
- }: React.MouseEvent<HTMLButtonElement>) => {
- stopPropagation();
- toggleSavedSound({ soundEffect: sound });
- };
-
- const handleAddToTimeline = async ({
- stopPropagation,
- }: React.MouseEvent<HTMLButtonElement>) => {
- stopPropagation();
- await addSoundToTimeline({ sound });
- };
-
- return (
- <div className="group flex items-center gap-3 opacity-100 hover:opacity-75">
- <button
- type="button"
- className="flex min-w-0 flex-1 items-center gap-3 text-left"
- onClick={handleClick}
- >
- <div className="bg-accent relative flex size-12 shrink-0 items-center justify-center overflow-hidden rounded-md">
- <div className="from-primary/20 absolute inset-0 bg-gradient-to-br to-transparent" />
- {isPlaying ? (
- <HugeiconsIcon icon={PauseIcon} className="size-5" />
- ) : (
- <HugeiconsIcon icon={PlayIcon} className="size-5" />
- )}
- </div>
-
- <div className="min-w-0 flex-1 overflow-hidden">
- <p className="truncate text-sm font-medium">{sound.name}</p>
- <span className="text-muted-foreground block truncate text-xs">
- {sound.username}
- </span>
- </div>
- </button>
-
- <div className="flex items-center gap-3 pr-2">
- <Button
- variant="text"
- size="icon"
- className="text-muted-foreground hover:text-foreground w-auto !opacity-100"
- onClick={handleAddToTimeline}
- title="Add to timeline"
- >
- <HugeiconsIcon icon={PlusSignIcon} />
- </Button>
- <Button
- variant="text"
- size="icon"
- className={`hover:text-foreground w-auto !opacity-100 ${
- isSaved
- ? "text-red-500 hover:text-red-600"
- : "text-muted-foreground"
- }`}
- onClick={handleSaveClick}
- title={isSaved ? "Remove from saved" : "Save sound"}
- >
- <HugeiconsIcon
- icon={FavouriteIcon}
- className={`${isSaved ? "fill-current" : ""}`}
- />
- </Button>
- </div>
- </div>
- );
-}
diff --git a/apps/web/src/components/editor/panels/assets/views/stickers.tsx b/apps/web/src/components/editor/panels/assets/views/stickers.tsx
deleted file mode 100644
index 318aaccb..00000000
--- a/apps/web/src/components/editor/panels/assets/views/stickers.tsx
+++ /dev/null
@@ -1,455 +0,0 @@
-"use client";
-
-import Image from "next/image";
-import type { CSSProperties } from "react";
-import { useEffect, useState } from "react";
-import { toast } from "sonner";
-import { DraggableItem } from "@/components/editor/panels/assets/draggable-item";
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import { Spinner } from "@/components/ui/spinner";
-import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
-import { useEditor } from "@/hooks/use-editor";
-import { resolveStickerIntrinsicSize } from "@/lib/stickers";
-import {
- buildGraphicElement,
- buildStickerElement,
-} from "@/lib/timeline/element-utils";
-import { STICKER_CATEGORIES } from "@/lib/stickers/categories";
-import { getRegionLabel, resolveQueryToRegions } from "@/lib/stickers";
-import { parseShapeStickerId } from "@/lib/stickers/providers/shapes";
-import type { TimelineDragData } from "@/lib/timeline/drag";
-import type {
- StickerBrowseSection,
- StickerCategory,
- StickerItem as StickerData,
-} from "@/lib/stickers";
-import { useStickersStore } from "@/stores/stickers-store";
-import { cn } from "@/utils/ui";
-import {
- HappyIcon,
-} from "@hugeicons/core-free-icons";
-import { HugeiconsIcon } from "@hugeicons/react";
-
-export function StickersView() {
- const {
- browseContent,
- browseStickers,
- searchQuery,
- searchStickers,
- selectedCategory,
- setSearchQuery,
- setSelectedCategory,
- viewMode,
- } = useStickersStore();
-
- useEffect(() => {
- if (viewMode === "browse" && !browseContent) {
- void browseStickers();
- }
- }, [browseContent, browseStickers, viewMode]);
-
- return (
- <div className="flex h-full flex-col py-2">
- <div className="px-2">
- <Input
- size="sm"
- variant="default"
- placeholder="Search..."
- value={searchQuery}
- onChange={(e) => {
- setSearchQuery({ query: e.target.value });
- void searchStickers({ query: e.target.value });
- }}
- showClearIcon
- onClear={() => {
- setSearchQuery({ query: "" });
- void searchStickers({ query: "" });
- }}
- className="w-full"
- containerClassName="w-full"
- />
- </div>
-
- <Tabs
- value={selectedCategory}
- onValueChange={(value) => {
- setSelectedCategory({ category: value as StickerCategory });
- }}
- variant="underline"
- className="mt-2 flex min-h-0 flex-1 flex-col"
- >
- <TabsList aria-label="Sticker categories">
- {Object.entries(STICKER_CATEGORIES).map(([key, label]) => (
- <TabsTrigger key={key} value={key}>
- {label}
- </TabsTrigger>
- ))}
- </TabsList>
- <div className="min-h-0 flex-1 overflow-y-auto px-4 pt-4">
- <StickersContentView />
- </div>
- </Tabs>
- </div>
- );
-}
-
-function StickerGrid({
- items,
- shouldCapSize = false,
-}: {
- items: StickerData[];
- shouldCapSize?: boolean;
-}) {
- const gridStyle: CSSProperties & {
- "--sticker-min": string;
- "--sticker-max"?: string;
- } = {
- gridTemplateColumns: shouldCapSize
- ? "repeat(auto-fill, minmax(var(--sticker-min, 80px), var(--sticker-max, 140px)))"
- : "repeat(auto-fill, minmax(var(--sticker-min, 80px), 1fr))",
- "--sticker-min": "80px",
- ...(shouldCapSize ? { "--sticker-max": "140px" } : {}),
- };
-
- return (
- <div className="grid gap-2" style={gridStyle}>
- {items.map((item) => (
- <StickerItem key={item.id} item={item} shouldCapSize={shouldCapSize} />
- ))}
- </div>
- );
-}
-
-function StickerRow({ items }: { items: StickerData[] }) {
- return (
- <div className="flex gap-2 overflow-x-auto pb-1 scrollbar-hidden">
- {items.map((item) => (
- <div key={item.id} className="w-20 shrink-0">
- <StickerItem item={item} shouldCapSize containerClassName="w-full" />
- </div>
- ))}
- </div>
- );
-}
-
-function EmptyView({ message }: { message: string }) {
- return (
- <div className="bg-background flex h-full flex-col items-center justify-center gap-3 p-4">
- <HugeiconsIcon
- icon={HappyIcon}
- className="text-muted-foreground size-10"
- />
- <div className="flex flex-col gap-2 text-center">
- <p className="text-lg font-medium">No stickers found</p>
- <p className="text-muted-foreground text-sm text-balance">{message}</p>
- </div>
- </div>
- );
-}
-
-function RegionBanner({ region }: { region: string }) {
- return (
- <div className="flex h-7 items-center gap-1.5 rounded-lg border border-sky-100 bg-sky-50 px-2">
- <svg
- width="12"
- height="12"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- strokeWidth="2"
- strokeLinecap="round"
- strokeLinejoin="round"
- className="shrink-0 text-sky-600"
- aria-hidden="true"
- >
- <path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z" />
- <circle cx="12" cy="10" r="3" />
- </svg>
- <span className="text-xs font-semibold text-sky-600">{region}</span>
- </div>
- );
-}
-
-function StickersContentView() {
- const {
- browseContent,
- clearRecentStickers,
- isBrowsing,
- isSearching,
- searchQuery,
- searchResults,
- selectedCategory,
- setSelectedCategory,
- viewMode,
- } = useStickersStore();
-
- if (viewMode === "search") {
- if (isSearching) {
- return (
- <div className="flex items-center justify-center py-8">
- <Spinner className="text-muted-foreground size-6" />
- </div>
- );
- }
-
- if (searchResults?.items.length) {
- const normalizedQuery = searchQuery.trim().toLowerCase();
- const isRegionSearch =
- selectedCategory === "flags" &&
- resolveQueryToRegions({ query: normalizedQuery }) !== null;
- const regionLabel = getRegionLabel({ query: normalizedQuery });
-
- return (
- <div className="flex flex-col gap-3 pb-4">
- {isRegionSearch && <RegionBanner region={regionLabel} />}
- <div className="flex items-center justify-between">
- <span className="text-muted-foreground text-sm">
- {searchResults.total} results
- </span>
- </div>
- <StickerGrid items={searchResults.items} />
- </div>
- );
- }
-
- // "all" tab search — sections are in browseContent, fall through to section rendering below
- if (selectedCategory !== "all" && searchQuery) {
- return <EmptyView message={`No stickers found for "${searchQuery}"`} />;
- }
- }
-
- if (isBrowsing && !browseContent) {
- return (
- <div className="flex items-center justify-center py-8">
- <Spinner className="text-muted-foreground size-6" />
- </div>
- );
- }
-
- if (!browseContent?.sections.length) {
- const categoryLabel = STICKER_CATEGORIES[selectedCategory];
- return (
- <EmptyView
- message={
- viewMode === "search"
- ? `No stickers found for "${searchQuery}"`
- : selectedCategory === "all"
- ? "No stickers available yet."
- : `No stickers available in ${categoryLabel.toLowerCase()} yet.`
- }
- />
- );
- }
-
- return (
- <div className="flex flex-col gap-4 pb-4">
- {browseContent.sections.map((section) => (
- <StickerSection
- key={section.id}
- section={section}
- onClearRecent={clearRecentStickers}
- onSeeAll={(category) => {
- setSelectedCategory({ category });
- }}
- />
- ))}
- </div>
- );
-}
-
-function StickerSection({
- section,
- onClearRecent,
- onSeeAll,
-}: {
- section: StickerBrowseSection;
- onClearRecent: () => void;
- onSeeAll: (category: StickerCategory) => void;
-}) {
- const hasHeader =
- Boolean(section.title) || section.id === "recent" || section.action;
-
- return (
- <div className="flex flex-col gap-2">
- {hasHeader && (
- <div className="flex items-center justify-between gap-2">
- {section.title ? (
- <p className="text-xs text-muted-foreground">{section.title}</p>
- ) : (
- <div />
- )}
-
- <div className="ml-auto flex items-center gap-2">
- {section.id === "recent" && (
- <Button
- onClick={onClearRecent}
- variant="text"
- size="sm"
- className="h-auto gap-1 p-0 text-xs text-muted-foreground"
- >
- Clear
- </Button>
- )}
-
- {section.action?.type === "see-all" && section.action.category && (
- <Button
- variant="text"
- size="sm"
- className="h-auto gap-1 p-0 text-xs text-primary"
- onClick={() => {
- onSeeAll(section.action?.category as StickerCategory);
- }}
- >
- See all
- </Button>
- )}
- </div>
- </div>
- )}
-
- {section.layout === "row" ? (
- <StickerRow items={section.items} />
- ) : (
- <StickerGrid items={section.items} />
- )}
- </div>
- );
-}
-
-interface StickerItemProps {
- item: StickerData;
- shouldCapSize?: boolean;
- containerClassName?: string;
-}
-
-function StickerItem({
- item,
- shouldCapSize = false,
- containerClassName,
-}: StickerItemProps) {
- const editor = useEditor();
- const { addToRecentStickers } = useStickersStore();
- const [isAdding, setIsAdding] = useState(false);
- const [hasImageError, setHasImageError] = useState(false);
-
- useEffect(() => {
- if (!item.id) {
- return;
- }
-
- setHasImageError(false);
- }, [item.id]);
-
- const displayName = item.name;
- const shapePreset =
- item.provider === "shapes" ? parseShapeStickerId({ stickerId: item.id }) : null;
-
- const handleAdd = async () => {
- setIsAdding(true);
- try {
- const currentTime = editor.playback.getCurrentTime();
-
- let element:
- | ReturnType<typeof buildGraphicElement>
- | ReturnType<typeof buildStickerElement>;
- if (shapePreset) {
- element = buildGraphicElement({
- definitionId: shapePreset.definitionId,
- name: shapePreset.name,
- startTime: currentTime,
- params: shapePreset.params,
- });
- } else {
- const { width: intrinsicWidth, height: intrinsicHeight } =
- await resolveStickerIntrinsicSize({ stickerId: item.id });
- element = buildStickerElement({
- stickerId: item.id,
- name: item.name,
- startTime: currentTime,
- intrinsicWidth,
- intrinsicHeight,
- });
- }
-
- editor.timeline.insertElement({
- placement: { mode: "auto" },
- element,
- });
-
- addToRecentStickers({ stickerId: item.id });
- } catch (error) {
- console.error("Failed to add sticker:", error);
- toast.error("Failed to add sticker to timeline");
- } finally {
- setIsAdding(false);
- }
- };
-
- const preview = (
- <div className="flex size-full items-center justify-center p-3">
- {hasImageError ? (
- <span className="text-muted-foreground text-center text-xs break-all">
- {displayName}
- </span>
- ) : (
- <Image
- src={item.previewUrl}
- alt={displayName}
- width={64}
- height={64}
- className="size-full object-contain"
- style={
- shouldCapSize
- ? {
- maxWidth: "var(--sticker-max, 160px)",
- maxHeight: "var(--sticker-max, 160px)",
- }
- : undefined
- }
- onError={() => setHasImageError(true)}
- loading="lazy"
- unoptimized
- />
- )}
- </div>
- );
-
- const dragData: TimelineDragData = shapePreset
- ? {
- id: item.id,
- type: "graphic",
- name: displayName,
- definitionId: shapePreset.definitionId,
- params: shapePreset.params ?? {},
- }
- : {
- id: item.id,
- type: "sticker",
- name: displayName,
- stickerId: item.id,
- };
-
- return (
- <div
- className={cn("relative", isAdding && "pointer-events-none opacity-50")}
- >
- <DraggableItem
- name={displayName}
- preview={preview}
- dragData={dragData}
- onAddToTimeline={handleAdd}
- aspectRatio={1}
- shouldShowLabel={false}
- isRounded
- variant="card"
- containerClassName={containerClassName ?? "w-full"}
- />
- {isAdding && (
- <div className="absolute inset-0 z-10 flex items-center justify-center rounded-md bg-black/60">
- <Spinner className="size-6 text-white" />
- </div>
- )}
- </div>
- );
-}
diff --git a/apps/web/src/components/editor/panels/assets/views/text.tsx b/apps/web/src/components/editor/panels/assets/views/text.tsx
deleted file mode 100644
index da295da3..00000000
--- a/apps/web/src/components/editor/panels/assets/views/text.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import { DraggableItem } from "@/components/editor/panels/assets/draggable-item";
-import { PanelView } from "@/components/editor/panels/assets/views/base-panel";
-import { useEditor } from "@/hooks/use-editor";
-import { DEFAULTS } from "@/lib/timeline/defaults";
-import { buildTextElement } from "@/lib/timeline/element-utils";
-
-export function TextView() {
- const editor = useEditor();
-
- const handleAddToTimeline = ({ currentTime }: { currentTime: number }) => {
- const activeScene = editor.scenes.getActiveScene();
- if (!activeScene) return;
-
- const element = buildTextElement({
- raw: DEFAULTS.text.element,
- startTime: currentTime,
- });
-
- editor.timeline.insertElement({
- element,
- placement: { mode: "auto" },
- });
- };
-
- return (
- <PanelView title="Text">
- <DraggableItem
- name="Default text"
- preview={
- <div className="bg-accent flex size-full items-center justify-center rounded">
- <span className="text-xs select-none">Default text</span>
- </div>
- }
- dragData={{
- id: "temp-text-id",
- type: DEFAULTS.text.element.type,
- name: DEFAULTS.text.element.name,
- content: DEFAULTS.text.element.content,
- }}
- aspectRatio={1}
- onAddToTimeline={handleAddToTimeline}
- shouldShowLabel={false}
- />
- </PanelView>
- );
-}