@umituz/react-native-video-editor 1.1.63 → 1.1.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/package.json +1 -1
  2. package/src/application/services/EditorService.ts +151 -0
  3. package/src/application/usecases/LayerUseCases.ts +192 -0
  4. package/src/infrastructure/repositories/LayerRepositoryImpl.ts +158 -0
  5. package/src/infrastructure/services/image-layer-operations.service.ts +1 -1
  6. package/src/infrastructure/services/shape-layer-operations.service.ts +1 -1
  7. package/src/infrastructure/services/text-layer-operations.service.ts +1 -1
  8. package/src/infrastructure/utils/debounce.utils.ts +69 -0
  9. package/src/infrastructure/utils/image-processing.utils.ts +73 -0
  10. package/src/infrastructure/utils/position-calculations.utils.ts +45 -161
  11. package/src/presentation/components/EditorTimeline.tsx +29 -11
  12. package/src/presentation/components/EditorToolPanel.tsx +71 -172
  13. package/src/presentation/components/LayerActionsMenu.tsx +97 -159
  14. package/src/presentation/components/SceneActionsMenu.tsx +34 -44
  15. package/src/presentation/components/SubtitleListPanel.tsx +55 -28
  16. package/src/presentation/components/SubtitleStylePicker.tsx +36 -156
  17. package/src/presentation/components/collage/CollageCanvas.tsx +2 -2
  18. package/src/presentation/components/collage/CollageLayoutSelector.tsx +0 -4
  19. package/src/presentation/components/draggable-layer/LayerContent.tsx +7 -2
  20. package/src/presentation/components/generic/ActionMenu.tsx +110 -0
  21. package/src/presentation/components/generic/Editor.tsx +65 -0
  22. package/src/presentation/components/generic/Selector.tsx +96 -0
  23. package/src/presentation/components/generic/Toolbar.tsx +77 -0
  24. package/src/presentation/components/image-layer/ImagePreview.tsx +7 -1
  25. package/src/presentation/components/shape-layer/ColorPickerHorizontal.tsx +20 -55
  26. package/src/presentation/components/subtitle/SubtitleListItem.tsx +4 -5
  27. package/src/presentation/components/subtitle/SubtitleModal.tsx +2 -2
  28. package/src/presentation/components/subtitle/useSubtitleForm.ts +1 -1
  29. package/src/presentation/components/text-layer/ColorPicker.tsx +21 -55
  30. package/src/presentation/components/text-layer/FontSizeSelector.tsx +19 -50
  31. package/src/presentation/components/text-layer/OptionSelector.tsx +18 -55
  32. package/src/presentation/components/text-layer/TextAlignSelector.tsx +24 -51
  33. package/src/presentation/hooks/generic/use-layer-form.hook.ts +19 -16
  34. package/src/presentation/hooks/generic/useForm.ts +99 -0
  35. package/src/presentation/hooks/generic/useList.ts +117 -0
  36. package/src/presentation/hooks/useDraggableLayerGestures.ts +28 -25
  37. package/src/presentation/hooks/useEditorPlayback.ts +19 -2
  38. package/src/presentation/hooks/useImageLayerForm.ts +9 -6
  39. package/src/presentation/hooks/useMenuActions.tsx +19 -4
  40. package/src/presentation/hooks/useTextLayerForm.ts +9 -6
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Generic Form Hook
3
+ * Replaces: useImageLayerForm, useTextLayerForm, useShapeLayerForm, useAudioLayerForm, useAnimationLayerForm
4
+ */
5
+
6
+ import { useState, useCallback, useMemo } from "react";
7
+
8
+ export interface FormValidator<T = unknown> {
9
+ required?: boolean;
10
+ validate?: (value: T) => string | undefined;
11
+ }
12
+
13
+ export interface FormConfig<T extends Record<string, unknown>> {
14
+ initialValues: T;
15
+ validators?: Partial<Record<keyof T, FormValidator>>;
16
+ onSubmit: (values: T) => void | Promise<void>;
17
+ }
18
+
19
+ export interface FormReturn<T extends Record<string, unknown>> {
20
+ values: T;
21
+ errors: Partial<Record<keyof T, string>>;
22
+ touched: Partial<Record<keyof T, boolean>>;
23
+ isValid: boolean;
24
+ isSubmitting: boolean;
25
+ setValue: <K extends keyof T>(field: K, value: T[K]) => void;
26
+ setError: <K extends keyof T>(field: K, error: string | undefined) => void;
27
+ setTouched: <K extends keyof T>(field: K, touched: boolean) => void;
28
+ handleSubmit: () => Promise<void>;
29
+ resetForm: () => void;
30
+ }
31
+
32
+ export function useForm<T extends Record<string, unknown>>({ initialValues, validators = {}, onSubmit }: FormConfig<T>): FormReturn<T> {
33
+ const [values, setValues] = useState<T>(initialValues);
34
+ const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
35
+ const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({});
36
+ const [isSubmitting, setIsSubmitting] = useState(false);
37
+
38
+ const validateField = useCallback(<K extends keyof T>(field: K, value: T[K]): string | undefined => {
39
+ const validator = validators[field];
40
+ if (!validator) return undefined;
41
+ if (validator.required && (value === undefined || value === null || value === "")) return "This field is required";
42
+ if (validator.validate) return validator.validate(value);
43
+ return undefined;
44
+ }, [validators]);
45
+
46
+ const validateForm = useCallback((): boolean => {
47
+ const newErrors: Partial<Record<keyof T, string>> = {};
48
+ let isValid = true;
49
+ (Object.keys(validators) as Array<keyof T>).forEach((field) => {
50
+ const error = validateField(field, values[field]);
51
+ if (error) { newErrors[field] = error; isValid = false; }
52
+ });
53
+ setErrors(newErrors);
54
+ return isValid;
55
+ }, [validators, values, validateField]);
56
+
57
+ const setValue = useCallback(<K extends keyof T>(field: K, value: T[K]) => {
58
+ setValues((prev) => ({ ...prev, [field]: value }));
59
+ if (touched[field]) {
60
+ const error = validateField(field, value);
61
+ setErrors((prev) => ({ ...prev, [field]: error }));
62
+ }
63
+ }, [touched, validateField]);
64
+
65
+ const setError = useCallback(<K extends keyof T>(field: K, error: string | undefined) => {
66
+ setErrors((prev) => ({ ...prev, [field]: error }));
67
+ }, []);
68
+
69
+ const setFieldTouched = useCallback(<K extends keyof T>(field: K, touchedValue: boolean) => {
70
+ setTouched((prev) => ({ ...prev, [field]: touchedValue }));
71
+ if (touchedValue) {
72
+ const error = validateField(field, values[field]);
73
+ setErrors((prev) => ({ ...prev, [field]: error }));
74
+ }
75
+ }, [values, validateField]);
76
+
77
+ const isValid = useMemo(() => Object.keys(validators).every((field) => !errors[field as keyof T]), [errors, validators]);
78
+
79
+ const handleSubmit = useCallback(async () => {
80
+ const allTouched = Object.keys(validators).reduce((acc, field) => ({ ...acc, [field]: true }), {} as Partial<Record<keyof T, boolean>>);
81
+ setTouched(allTouched);
82
+ const valid = validateForm();
83
+ if (!valid) return;
84
+ setIsSubmitting(true);
85
+ try {
86
+ await onSubmit(values);
87
+ } finally {
88
+ setIsSubmitting(false);
89
+ }
90
+ }, [values, validators, onSubmit, validateForm]);
91
+
92
+ const resetForm = useCallback(() => {
93
+ setValues(initialValues);
94
+ setErrors({});
95
+ setTouched({});
96
+ }, [initialValues]);
97
+
98
+ return { values, errors, touched, isValid, isSubmitting, setValue, setError, setTouched: setFieldTouched, handleSubmit, resetForm };
99
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Generic List Hook
3
+ * Replaces: SubtitleListPanel, LayerList, SceneList patterns
4
+ */
5
+
6
+ import { useState, useCallback, useMemo } from "react";
7
+
8
+ export interface ListConfig<T> {
9
+ items: T[];
10
+ keyExtractor: (item: T) => string;
11
+ initialSelection?: string[];
12
+ multiSelect?: boolean;
13
+ }
14
+
15
+ export interface ListReturn<T> {
16
+ selectedIds: Set<string>;
17
+ selectedItems: T[];
18
+ isSelected: (id: string) => boolean;
19
+ toggleSelection: (id: string) => void;
20
+ selectAll: () => void;
21
+ clearSelection: () => void;
22
+ filteredItems: T[];
23
+ filter: (predicate: (item: T) => boolean) => void;
24
+ clearFilter: () => void;
25
+ }
26
+
27
+ export function useList<T>({ items, keyExtractor, initialSelection = [], multiSelect = false }: ListConfig<T>): ListReturn<T> {
28
+ const [selectedIds, setSelectedIds] = useState<Set<string>>(() => new Set(initialSelection));
29
+ const [filterPredicate, setFilterPredicate] = useState<((item: T) => boolean) | null>(null);
30
+
31
+ const selectedItems = useMemo(() => items.filter((item) => selectedIds.has(keyExtractor(item))), [items, selectedIds, keyExtractor]);
32
+ const filteredItems = useMemo(() => filterPredicate ? items.filter(filterPredicate) : items, [items, filterPredicate]);
33
+
34
+ const isSelected = useCallback((id: string) => selectedIds.has(id), [selectedIds]);
35
+
36
+ const toggleSelection = useCallback((id: string) => {
37
+ setSelectedIds((prev) => {
38
+ const newSet = new Set(prev);
39
+ if (multiSelect) {
40
+ if (newSet.has(id)) {
41
+ newSet.delete(id);
42
+ } else {
43
+ newSet.add(id);
44
+ }
45
+ } else {
46
+ newSet.clear();
47
+ newSet.add(id);
48
+ }
49
+ return newSet;
50
+ });
51
+ }, [multiSelect]);
52
+
53
+ const selectAll = useCallback(() => setSelectedIds(new Set(items.map(keyExtractor))), [items, keyExtractor]);
54
+ const clearSelection = useCallback(() => setSelectedIds(new Set()), []);
55
+ const filter = useCallback((predicate: (item: T) => boolean) => setFilterPredicate(() => predicate), []);
56
+ const clearFilter = useCallback(() => setFilterPredicate(null), []);
57
+
58
+ return { selectedIds, selectedItems, isSelected, toggleSelection, selectAll, clearSelection, filteredItems, filter, clearFilter };
59
+ }
60
+
61
+ export interface UseSelectionConfig {
62
+ multiSelect?: boolean;
63
+ maxSelections?: number;
64
+ }
65
+
66
+ export interface UseSelectionReturn {
67
+ selectedIds: Set<string>;
68
+ isSelected: (id: string) => boolean;
69
+ toggleSelection: (id: string) => void;
70
+ select: (id: string) => void;
71
+ deselect: (id: string) => void;
72
+ clearSelection: () => void;
73
+ selectionCount: number;
74
+ }
75
+
76
+ export function useSelection({ multiSelect = false, maxSelections }: UseSelectionConfig = {}): UseSelectionReturn {
77
+ const [selectedIds, setSelectedIds] = useState<Set<string>>(() => new Set());
78
+
79
+ const isSelected = useCallback((id: string) => selectedIds.has(id), [selectedIds]);
80
+ const selectionCount = selectedIds.size;
81
+
82
+ const select = useCallback((id: string) => {
83
+ setSelectedIds((prev) => {
84
+ if (!multiSelect) return new Set([id]);
85
+ if (maxSelections && prev.size >= maxSelections && !prev.has(id)) return prev;
86
+ const newSet = new Set(prev);
87
+ newSet.add(id);
88
+ return newSet;
89
+ });
90
+ }, [multiSelect, maxSelections]);
91
+
92
+ const deselect = useCallback((id: string) => {
93
+ setSelectedIds((prev) => {
94
+ const newSet = new Set(prev);
95
+ newSet.delete(id);
96
+ return newSet;
97
+ });
98
+ }, []);
99
+
100
+ const toggleSelection = useCallback((id: string) => {
101
+ setSelectedIds((prev) => {
102
+ if (!multiSelect) return new Set([id]);
103
+ const newSet = new Set(prev);
104
+ if (newSet.has(id)) {
105
+ newSet.delete(id);
106
+ } else {
107
+ if (maxSelections && newSet.size >= maxSelections) return prev;
108
+ newSet.add(id);
109
+ }
110
+ return newSet;
111
+ });
112
+ }, [multiSelect, maxSelections]);
113
+
114
+ const clearSelection = useCallback(() => setSelectedIds(new Set()), []);
115
+
116
+ return { selectedIds, isSelected, toggleSelection, select, deselect, clearSelection, selectionCount };
117
+ }
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * useDraggableLayerGestures Hook
3
3
  * Manages gesture handling for draggable layers
4
+ * PERFORMANCE: Uses runOnJS for state updates to prevent UI thread blocking
4
5
  */
5
6
 
6
7
  import { useState, useRef, useCallback, useEffect } from "react";
7
8
  import { Gesture } from "react-native-gesture-handler";
9
+ import { runOnJS } from "react-native-reanimated";
8
10
 
9
11
  interface UseDraggableLayerGesturesParams {
10
12
  initialX: number;
@@ -74,22 +76,23 @@ export function useDraggableLayerGestures({
74
76
  const gestureHandler = Gesture.Pan()
75
77
  .onStart(() => {
76
78
  startRef.current = { ...state, x: state.x, y: state.y };
77
- onSelect();
79
+ runOnJS(onSelect)();
78
80
  })
79
81
  .onUpdate((event) => {
82
+ 'worklet';
80
83
  const newX = startRef.current.x + event.translationX;
81
84
  const newY = startRef.current.y + event.translationY;
82
- setState((prev) => ({ ...prev, x: newX, y: newY }));
85
+ // Use runOnJS for state updates to prevent blocking UI thread
86
+ runOnJS(setState)({ x: newX, y: newY, width: state.width, height: state.height });
83
87
  })
84
88
  .onEnd(() => {
85
- setState((prev) => {
86
- const clampedX = clamp(prev.x, 0, canvasWidth - prev.width);
87
- const clampedY = clamp(prev.y, 0, canvasHeight - prev.height);
88
- const newX = (clampedX / canvasWidth) * 100;
89
- const newY = (clampedY / canvasHeight) * 100;
90
- onPositionChange(newX, newY);
91
- return { ...prev, x: clampedX, y: clampedY };
92
- });
89
+ 'worklet';
90
+ const clampedX = clamp(state.x, 0, canvasWidth - state.width);
91
+ const clampedY = clamp(state.y, 0, canvasHeight - state.height);
92
+ const newX = (clampedX / canvasWidth) * 100;
93
+ const newY = (clampedY / canvasHeight) * 100;
94
+ runOnJS(setState)({ x: clampedX, y: clampedY, width: state.width, height: state.height });
95
+ runOnJS(onPositionChange)(newX, newY);
93
96
  });
94
97
 
95
98
  const createResizeHandler = (
@@ -98,10 +101,12 @@ export function useDraggableLayerGestures({
98
101
  ) => {
99
102
  return Gesture.Pan()
100
103
  .onStart(() => {
104
+ 'worklet';
101
105
  startRef.current = { ...state };
102
- onSelect();
106
+ runOnJS(onSelect)();
103
107
  })
104
108
  .onUpdate((event) => {
109
+ 'worklet';
105
110
  const newWidth = Math.max(MIN_SIZE, startRef.current.width + deltaX(event.translationX));
106
111
  const newHeight = Math.max(MIN_SIZE, startRef.current.height + deltaY(event.translationY));
107
112
  const clampedWidth = Math.min(newWidth, canvasWidth - startRef.current.x);
@@ -117,24 +122,22 @@ export function useDraggableLayerGestures({
117
122
  newY = Math.max(0, startRef.current.y + (startRef.current.height - clampedHeight));
118
123
  }
119
124
 
120
- setState({ x: newX, y: newY, width: clampedWidth, height: clampedHeight });
125
+ runOnJS(setState)({ x: newX, y: newY, width: clampedWidth, height: clampedHeight });
121
126
  })
122
127
  .onEnd(() => {
123
- setState((prev) => {
124
- // Clamp position to canvas bounds
125
- const clampedX = Math.max(0, Math.min(prev.x, canvasWidth - prev.width));
126
- const clampedY = Math.max(0, Math.min(prev.y, canvasHeight - prev.height));
128
+ 'worklet';
129
+ // Clamp position to canvas bounds
130
+ const clampedX = Math.max(0, Math.min(state.x, canvasWidth - state.width));
131
+ const clampedY = Math.max(0, Math.min(state.y, canvasHeight - state.height));
127
132
 
128
- const newWidth = (prev.width / canvasWidth) * 100;
129
- const newHeight = (prev.height / canvasHeight) * 100;
130
- const newX = (clampedX / canvasWidth) * 100;
131
- const newY = (clampedY / canvasHeight) * 100;
132
-
133
- onSizeChange(newWidth, newHeight);
134
- onPositionChange(newX, newY);
133
+ const newWidth = (state.width / canvasWidth) * 100;
134
+ const newHeight = (state.height / canvasHeight) * 100;
135
+ const newX = (clampedX / canvasWidth) * 100;
136
+ const newY = (clampedY / canvasHeight) * 100;
135
137
 
136
- return { ...prev, x: clampedX, y: clampedY };
137
- });
138
+ runOnJS(onSizeChange)(newWidth, newHeight);
139
+ runOnJS(onPositionChange)(newX, newY);
140
+ runOnJS(setState)({ x: clampedX, y: clampedY, width: state.width, height: state.height });
138
141
  });
139
142
  };
140
143
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * useEditorPlayback Hook
3
3
  * Single Responsibility: Playback control for editor
4
- * Optimized for performance with stable animation loop
4
+ * PERFORMANCE: Optimized with frame throttling (30fps target) and stable refs
5
5
  */
6
6
 
7
7
  import { useState, useEffect, useCallback, useRef } from "react";
@@ -12,6 +12,11 @@ import {
12
12
  isTimeAtEnd,
13
13
  } from "../../infrastructure/utils/time-calculations.utils";
14
14
 
15
+ // PERFORMANCE: Target 30fps instead of 60fps to reduce CPU load
16
+ // 60fps = 16.67ms, 30fps = 33.33ms between frames
17
+ const TARGET_FPS = 30;
18
+ const FRAME_INTERVAL = 1000 / TARGET_FPS;
19
+
15
20
  interface UseEditorPlaybackParams {
16
21
  currentScene: Scene | undefined;
17
22
  }
@@ -33,6 +38,7 @@ export function useEditorPlayback({
33
38
  // Use refs to avoid re-creating animation loop on every render
34
39
  const animationFrameRef = useRef<number | null>(null);
35
40
  const lastTimestampRef = useRef<number>(0);
41
+ const lastFrameTimeRef = useRef<number>(0); // Track frame time for throttling
36
42
  const isPlayingRef = useRef(isPlaying);
37
43
  const currentSceneRef = useRef(currentScene);
38
44
  const durationRef = useRef(0);
@@ -49,7 +55,7 @@ export function useEditorPlayback({
49
55
  }
50
56
  }, [currentScene]);
51
57
 
52
- // Video playback animation loop - optimized with refs
58
+ // Video playback animation loop - optimized with refs and frame throttling
53
59
  useEffect(() => {
54
60
  if (!currentScene) return;
55
61
 
@@ -60,9 +66,19 @@ export function useEditorPlayback({
60
66
  return;
61
67
  }
62
68
 
69
+ // PERFORMANCE: Frame throttling - skip frames to maintain target FPS
70
+ if (lastFrameTimeRef.current > 0) {
71
+ const elapsed = timestamp - lastFrameTimeRef.current;
72
+ if (elapsed < FRAME_INTERVAL) {
73
+ animationFrameRef.current = requestAnimationFrame(animate);
74
+ return;
75
+ }
76
+ }
77
+
63
78
  if (lastTimestampRef.current === 0) {
64
79
  lastTimestampRef.current = timestamp;
65
80
  }
81
+ lastFrameTimeRef.current = timestamp;
66
82
 
67
83
  const deltaTime = calculateDelta(timestamp, lastTimestampRef.current);
68
84
  lastTimestampRef.current = timestamp;
@@ -87,6 +103,7 @@ export function useEditorPlayback({
87
103
 
88
104
  if (isPlaying) {
89
105
  lastTimestampRef.current = 0;
106
+ lastFrameTimeRef.current = 0;
90
107
  animationFrameRef.current = requestAnimationFrame(animate);
91
108
  }
92
109
 
@@ -4,11 +4,12 @@
4
4
  */
5
5
 
6
6
  import type { ImageLayer } from "../../domain/entities/video-project.types";
7
- import { useLayerForm } from "./generic/use-layer-form.hook";
7
+ import { useLayerForm, type UseLayerFormConfig } from "./generic/use-layer-form.hook";
8
8
 
9
9
  interface ImageLayerFormState {
10
10
  imageUri: string;
11
11
  opacity: number;
12
+ [key: string]: unknown;
12
13
  }
13
14
 
14
15
  interface UseImageLayerFormReturn {
@@ -25,14 +26,14 @@ interface UseImageLayerFormReturn {
25
26
  export function useImageLayerForm(
26
27
  initialLayer?: ImageLayer,
27
28
  ): UseImageLayerFormReturn {
28
- const form = useLayerForm<ImageLayerFormState>({
29
+ const config: UseLayerFormConfig<ImageLayerFormState> = {
29
30
  initialValues: {
30
31
  imageUri: initialLayer?.uri || "",
31
32
  opacity: initialLayer?.opacity || 1,
32
33
  },
33
34
  validators: {
34
- imageUri: (value) => {
35
- if (!value || value.trim().length === 0) {
35
+ imageUri: (value: unknown) => {
36
+ if (!value || (typeof value === "string" && value.trim().length === 0)) {
36
37
  return "Image URI is required";
37
38
  }
38
39
  return null;
@@ -41,8 +42,10 @@ export function useImageLayerForm(
41
42
  buildData: (formState) => ({
42
43
  uri: formState.imageUri,
43
44
  opacity: formState.opacity,
44
- }),
45
- });
45
+ } as Partial<ImageLayer>),
46
+ };
47
+
48
+ const form = useLayerForm<ImageLayerFormState, Partial<ImageLayer>>(config);
46
49
 
47
50
  const setImageUri = (uri: string) => form.updateField("imageUri", uri);
48
51
  const setOpacity = (opacity: number) => form.updateField("opacity", opacity);
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * useMenuActions Hook
3
3
  * Single Responsibility: Menu action handlers (layer actions menu)
4
+ * MEMORY: Properly cleans up timers to prevent memory leaks
4
5
  */
5
6
 
6
- import { useCallback } from "react";
7
+ import { useCallback, useEffect, useRef } from "react";
7
8
  import { LayerActionsMenu } from "../components/LayerActionsMenu";
8
9
  import type { Layer } from "../../domain/entities/video-project.types";
9
10
  import type { UseEditorLayersReturn } from "./useEditorLayers";
@@ -30,6 +31,17 @@ export function useMenuActions({
30
31
  }: UseMenuActionsParams): UseMenuActionsReturn {
31
32
  const { openBottomSheet, closeBottomSheet } = bottomSheet;
32
33
 
34
+ // MEMORY: Track all timers for cleanup
35
+ const timersRef = useRef<ReturnType<typeof setTimeout>[]>([]);
36
+
37
+ // MEMORY: Clean up all timers on unmount
38
+ useEffect(() => {
39
+ return () => {
40
+ timersRef.current.forEach((timer) => clearTimeout(timer));
41
+ timersRef.current = [];
42
+ };
43
+ }, []);
44
+
33
45
  const handleLayerActionsPress = useCallback(
34
46
  (layer: Layer) => {
35
47
  openBottomSheet({
@@ -39,15 +51,18 @@ export function useMenuActions({
39
51
  layer={layer}
40
52
  onEditText={() => {
41
53
  closeBottomSheet();
42
- setTimeout(() => handleEditLayer(), 300);
54
+ const timer = setTimeout(() => handleEditLayer(), 300);
55
+ timersRef.current.push(timer);
43
56
  }}
44
57
  onEditImage={() => {
45
58
  closeBottomSheet();
46
- setTimeout(() => handleEditImageLayer(layer.id), 300);
59
+ const timer = setTimeout(() => handleEditImageLayer(layer.id), 300);
60
+ timersRef.current.push(timer);
47
61
  }}
48
62
  onAnimate={() => {
49
63
  closeBottomSheet();
50
- setTimeout(() => handleAnimate(layer.id), 300);
64
+ const timer = setTimeout(() => handleAnimate(layer.id), 300);
65
+ timersRef.current.push(timer);
51
66
  }}
52
67
  onDuplicate={() => {
53
68
  closeBottomSheet();
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
7
7
  import type { TextLayer } from "../../domain/entities/video-project.types";
8
- import { useLayerForm } from "./generic/use-layer-form.hook";
8
+ import { useLayerForm, type UseLayerFormConfig } from "./generic/use-layer-form.hook";
9
9
 
10
10
  export interface TextLayerFormState {
11
11
  text: string;
@@ -14,6 +14,7 @@ export interface TextLayerFormState {
14
14
  fontWeight: "normal" | "bold" | "300" | "700";
15
15
  color: string;
16
16
  textAlign: "left" | "center" | "right";
17
+ [key: string]: unknown;
17
18
  }
18
19
 
19
20
  interface UseTextLayerFormReturn {
@@ -36,7 +37,7 @@ export function useTextLayerForm(
36
37
  ): UseTextLayerFormReturn {
37
38
  const tokens = useAppDesignTokens();
38
39
 
39
- const form = useLayerForm<TextLayerFormState>({
40
+ const config: UseLayerFormConfig<TextLayerFormState> = {
40
41
  initialValues: {
41
42
  text: initialLayer?.content || "",
42
43
  fontSize: initialLayer?.fontSize || 48,
@@ -47,8 +48,8 @@ export function useTextLayerForm(
47
48
  textAlign: initialLayer?.textAlign || "center",
48
49
  },
49
50
  validators: {
50
- text: (value) => {
51
- if (!value || value.trim().length === 0) {
51
+ text: (value: unknown) => {
52
+ if (!value || (typeof value === "string" && value.trim().length === 0)) {
52
53
  return "Text content is required";
53
54
  }
54
55
  return null;
@@ -61,8 +62,10 @@ export function useTextLayerForm(
61
62
  fontWeight: formState.fontWeight,
62
63
  color: formState.color,
63
64
  textAlign: formState.textAlign,
64
- }),
65
- });
65
+ } as Partial<TextLayer>),
66
+ };
67
+
68
+ const form = useLayerForm<TextLayerFormState, Partial<TextLayer>>(config);
66
69
 
67
70
  const setText = (text: string) => form.updateField("text", text);
68
71
  const setFontSize = (size: number) => form.updateField("fontSize", size);