@technotoil/image-video-editor 0.1.0

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 (99) hide show
  1. package/ImageVideoEditor.podspec +21 -0
  2. package/README.md +136 -0
  3. package/android/build.gradle +76 -0
  4. package/android/gradle.properties +5 -0
  5. package/android/src/main/AndroidManifest.xml +13 -0
  6. package/android/src/main/java/com/technotoil/image_videoeditor/FrameGrabberModule.kt +67 -0
  7. package/android/src/main/java/com/technotoil/image_videoeditor/MediaEditorModule.kt +548 -0
  8. package/android/src/main/java/com/technotoil/image_videoeditor/MediaFileUtils.kt +29 -0
  9. package/android/src/main/java/com/technotoil/image_videoeditor/MediaLibraryModule.kt +305 -0
  10. package/android/src/main/java/com/technotoil/image_videoeditor/MediaPackage.kt +26 -0
  11. package/android/src/main/java/com/technotoil/image_videoeditor/MediaPickerModule.kt +111 -0
  12. package/android/src/main/java/com/technotoil/image_videoeditor/MediaPlayerModule.kt +34 -0
  13. package/android/src/main/java/com/technotoil/image_videoeditor/RNCameraViewManager.kt +761 -0
  14. package/android/src/main/java/com/technotoil/image_videoeditor/RNVideoPreviewManager.kt +317 -0
  15. package/ios/PrivacyInfo.xcprivacy +38 -0
  16. package/ios/RNCameraViewManager.m +420 -0
  17. package/ios/RNFrameGrabber.m +61 -0
  18. package/ios/RNMediaEditor.m +905 -0
  19. package/ios/RNMediaLibrary.m +389 -0
  20. package/ios/RNMediaPicker.m +144 -0
  21. package/ios/RNMediaPlayer.m +73 -0
  22. package/ios/RNVideoPreviewManager.m +263 -0
  23. package/ios/frames/film_vintage.png +0 -0
  24. package/ios/frames/floral_gold.png +0 -0
  25. package/ios/frames/minimal_double.png +0 -0
  26. package/ios/frames/polaroid_white.png +0 -0
  27. package/ios/frames/watercolor_floral.png +0 -0
  28. package/lib/module/assets/frames/film_vintage.png +0 -0
  29. package/lib/module/assets/frames/floral_gold.png +0 -0
  30. package/lib/module/assets/frames/minimal_double.png +0 -0
  31. package/lib/module/assets/frames/polaroid_white.png +0 -0
  32. package/lib/module/assets/frames/watercolor_floral.png +0 -0
  33. package/lib/module/components/VideoEditor.js +156 -0
  34. package/lib/module/components/VideoEditor.js.map +1 -0
  35. package/lib/module/index.js +4 -0
  36. package/lib/module/index.js.map +1 -0
  37. package/lib/module/native/CameraView.js +104 -0
  38. package/lib/module/native/CameraView.js.map +1 -0
  39. package/lib/module/native/FrameGrabber.js +13 -0
  40. package/lib/module/native/FrameGrabber.js.map +1 -0
  41. package/lib/module/native/MediaEditor.js +19 -0
  42. package/lib/module/native/MediaEditor.js.map +1 -0
  43. package/lib/module/native/MediaLibrary.js +37 -0
  44. package/lib/module/native/MediaLibrary.js.map +1 -0
  45. package/lib/module/native/MediaPicker.js +13 -0
  46. package/lib/module/native/MediaPicker.js.map +1 -0
  47. package/lib/module/native/MediaPlayer.js +13 -0
  48. package/lib/module/native/MediaPlayer.js.map +1 -0
  49. package/lib/module/native/VideoPreview.js +12 -0
  50. package/lib/module/native/VideoPreview.js.map +1 -0
  51. package/lib/module/package.json +1 -0
  52. package/lib/module/screens/CropScreen.js +1211 -0
  53. package/lib/module/screens/CropScreen.js.map +1 -0
  54. package/lib/module/screens/EditorScreen.js +5752 -0
  55. package/lib/module/screens/EditorScreen.js.map +1 -0
  56. package/lib/module/screens/ExportScreen.js +289 -0
  57. package/lib/module/screens/ExportScreen.js.map +1 -0
  58. package/lib/module/screens/GalleryScreen.js +505 -0
  59. package/lib/module/screens/GalleryScreen.js.map +1 -0
  60. package/lib/module/screens/PickScreen.js +1195 -0
  61. package/lib/module/screens/PickScreen.js.map +1 -0
  62. package/lib/module/types.js +2 -0
  63. package/lib/module/types.js.map +1 -0
  64. package/lib/typescript/src/components/VideoEditor.d.ts +13 -0
  65. package/lib/typescript/src/index.d.ts +2 -0
  66. package/lib/typescript/src/native/CameraView.d.ts +23 -0
  67. package/lib/typescript/src/native/FrameGrabber.d.ts +2 -0
  68. package/lib/typescript/src/native/MediaEditor.d.ts +3 -0
  69. package/lib/typescript/src/native/MediaLibrary.d.ts +16 -0
  70. package/lib/typescript/src/native/MediaPicker.d.ts +2 -0
  71. package/lib/typescript/src/native/MediaPlayer.d.ts +1 -0
  72. package/lib/typescript/src/native/VideoPreview.d.ts +19 -0
  73. package/lib/typescript/src/screens/CropScreen.d.ts +9 -0
  74. package/lib/typescript/src/screens/EditorScreen.d.ts +10 -0
  75. package/lib/typescript/src/screens/ExportScreen.d.ts +9 -0
  76. package/lib/typescript/src/screens/GalleryScreen.d.ts +8 -0
  77. package/lib/typescript/src/screens/PickScreen.d.ts +13 -0
  78. package/lib/typescript/src/types.d.ts +58 -0
  79. package/package.json +101 -0
  80. package/src/assets/frames/film_vintage.png +0 -0
  81. package/src/assets/frames/floral_gold.png +0 -0
  82. package/src/assets/frames/minimal_double.png +0 -0
  83. package/src/assets/frames/polaroid_white.png +0 -0
  84. package/src/assets/frames/watercolor_floral.png +0 -0
  85. package/src/components/VideoEditor.tsx +182 -0
  86. package/src/index.tsx +2 -0
  87. package/src/native/CameraView.tsx +95 -0
  88. package/src/native/FrameGrabber.ts +21 -0
  89. package/src/native/MediaEditor.ts +33 -0
  90. package/src/native/MediaLibrary.ts +69 -0
  91. package/src/native/MediaPicker.ts +17 -0
  92. package/src/native/MediaPlayer.ts +16 -0
  93. package/src/native/VideoPreview.tsx +20 -0
  94. package/src/screens/CropScreen.tsx +968 -0
  95. package/src/screens/EditorScreen.tsx +4517 -0
  96. package/src/screens/ExportScreen.tsx +282 -0
  97. package/src/screens/GalleryScreen.tsx +412 -0
  98. package/src/screens/PickScreen.tsx +1094 -0
  99. package/src/types.ts +58 -0
@@ -0,0 +1,4517 @@
1
+ import React, { useMemo, useState, useEffect, useRef } from 'react';
2
+ import {
3
+ Alert,
4
+ Image,
5
+ Pressable,
6
+ ScrollView,
7
+ StyleSheet,
8
+ Text,
9
+ View,
10
+ Dimensions,
11
+ PanResponder,
12
+ Platform,
13
+ TextInput,
14
+ Modal,
15
+ FlatList,
16
+ ActivityIndicator,
17
+ } from 'react-native';
18
+ import { editImage, trimVideo } from '../native/MediaEditor';
19
+ import { captureFrame } from '../native/FrameGrabber';
20
+ import { saveToGallery } from '../native/MediaLibrary';
21
+ import { VideoPreview } from '../native/VideoPreview';
22
+ import Video from 'react-native-video';
23
+ import Ionicons from 'react-native-vector-icons/Ionicons';
24
+ import type { ImageEditOptions, MediaItem, MusicTrack } from '../types';
25
+
26
+
27
+ const SCREEN_WIDTH = Dimensions.get('window').width;
28
+ const TIMELINE_WIDTH = SCREEN_WIDTH - 40;
29
+ const HANDLE_SIZE = 24;
30
+ const CARD_WIDTH = SCREEN_WIDTH * 0.76;
31
+ const CARD_MARGIN = 10;
32
+ const SNAP_INTERVAL = CARD_WIDTH + CARD_MARGIN * 2;
33
+
34
+ // ── Frame overlays ──────────────────────────────────────────────────────────
35
+ const FRAME_IMAGES: Record<string, any> = {
36
+ floral_gold: require('../assets/frames/floral_gold.png'),
37
+ film_vintage: require('../assets/frames/film_vintage.png'),
38
+ minimal_double: require('../assets/frames/minimal_double.png'),
39
+ polaroid_white: require('../assets/frames/polaroid_white.png'),
40
+ watercolor_floral: require('../assets/frames/watercolor_floral.png'),
41
+ };
42
+
43
+ const FRAME_CONFIGS: Record<string, { scale: number; offsetY?: number }> = {
44
+ floral_gold: { scale: 0.82 },
45
+ film_vintage: { scale: 0.85 },
46
+ minimal_double: { scale: 0.92 },
47
+ polaroid_white: { scale: 0.72, offsetY: -0.05 },
48
+ watercolor_floral: { scale: 0.78 },
49
+ };
50
+ const FRAME_LIST: { key: string; label: string }[] = [
51
+ { key: 'floral_gold', label: 'Gold Floral' },
52
+ { key: 'film_vintage', label: 'Film' },
53
+ { key: 'minimal_double', label: 'Minimal' },
54
+ { key: 'polaroid_white', label: 'Polaroid' },
55
+ { key: 'watercolor_floral', label: 'Watercolor' },
56
+ ];
57
+ const DUMMY_MUSIC_LIST: MusicTrack[] = [
58
+ {
59
+ id: '1',
60
+ title: 'Sunny Days',
61
+ artist: 'Lofi Dreamer',
62
+ url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3',
63
+ duration: '6:12',
64
+ cover: 'https://images.unsplash.com/photo-1511671782779-c97d3d27a1d4?w=150&auto=format&fit=crop&q=60',
65
+ },
66
+ {
67
+ id: '2',
68
+ title: 'Urban Groove',
69
+ artist: 'Beatmaster',
70
+ url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3',
71
+ duration: '7:05',
72
+ cover: 'https://images.unsplash.com/photo-1470225620780-dba8ba36b745?w=150&auto=format&fit=crop&q=60',
73
+ },
74
+ {
75
+ id: '3',
76
+ title: 'Calm Waters',
77
+ artist: 'Ambient Sound',
78
+ url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-3.mp3',
79
+ duration: '5:44',
80
+ cover: 'https://images.unsplash.com/photo-1459749411175-04bf5292ceea?w=150&auto=format&fit=crop&q=60',
81
+ },
82
+ {
83
+ id: '4',
84
+ title: 'Epic Journey',
85
+ artist: 'Cinematic Orchestra',
86
+ url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-4.mp3',
87
+ duration: '5:02',
88
+ cover: 'https://images.unsplash.com/photo-1506157786151-b8491531f063?w=150&auto=format&fit=crop&q=60',
89
+ },
90
+ {
91
+ id: '5',
92
+ title: 'Original Audio',
93
+ artist: 'vivek_saaraswat',
94
+ url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-5.mp3',
95
+ duration: '0:20',
96
+ cover: 'https://images.unsplash.com/photo-1514525253161-7a46d19cd819?w=150&auto=format&fit=crop&q=60',
97
+ },
98
+ {
99
+ id: '6',
100
+ title: 'Baapu Jaisa Insan',
101
+ artist: 'Nitin Sharma, Mr Dutt',
102
+ url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-6.mp3',
103
+ duration: '0:35',
104
+ cover: 'https://images.unsplash.com/photo-1498038432885-c6f3f1b912ee?w=150&auto=format&fit=crop&q=60',
105
+ },
106
+ {
107
+ id: '7',
108
+ title: 'Poonam Kero Chand',
109
+ artist: 'Raju Mewadi, Twinkle Vaishn',
110
+ url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-7.mp3',
111
+ duration: '0:45',
112
+ cover: 'https://images.unsplash.com/photo-1511379938547-c1f69419868d?w=150&auto=format&fit=crop&q=60',
113
+ },
114
+ {
115
+ id: '8',
116
+ title: 'Kesariya Tera',
117
+ artist: 'Arijit Singh',
118
+ url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-8.mp3',
119
+ duration: '0:30',
120
+ cover: 'https://images.unsplash.com/photo-1487180142328-0c4e37023af5?w=150&auto=format&fit=crop&q=60',
121
+ },
122
+ {
123
+ id: '9',
124
+ title: 'Calm Down',
125
+ artist: 'Rema, Selena Gomez',
126
+ url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-9.mp3',
127
+ duration: '0:25',
128
+ cover: 'https://images.unsplash.com/photo-1508700115892-45ecd05ae2ad?w=150&auto=format&fit=crop&q=60',
129
+ },
130
+ {
131
+ id: '10',
132
+ title: 'Shape of You',
133
+ artist: 'Ed Sheeran',
134
+ url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-10.mp3',
135
+ duration: '0:40',
136
+ cover: 'https://images.unsplash.com/photo-1453090927415-5f45085b65c0?w=150&auto=format&fit=crop&q=60',
137
+ },
138
+ {
139
+ id: 'c1',
140
+ title: 'Custom Beat 01',
141
+ artist: 'My Studio',
142
+ url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-11.mp3',
143
+ duration: '4:15',
144
+ cover: 'https://images.unsplash.com/photo-1484755560693-a4074577af3a?w=150&auto=format&fit=crop&q=60',
145
+ isCustom: true,
146
+ },
147
+ {
148
+ id: 'c2',
149
+ title: 'Acoustic Guitar Loop',
150
+ artist: 'My Audio',
151
+ url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-12.mp3',
152
+ duration: '3:45',
153
+ cover: 'https://images.unsplash.com/photo-1510915228340-29c85a43dcfe?w=150&auto=format&fit=crop&q=60',
154
+ isCustom: true,
155
+ },
156
+ {
157
+ id: 'c3',
158
+ title: 'Vibrant Synthwave',
159
+ artist: 'Draft Beats',
160
+ url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-13.mp3',
161
+ duration: '5:12',
162
+ cover: 'https://images.unsplash.com/photo-1511671782779-c97d3d27a1d4?w=150&auto=format&fit=crop&q=60',
163
+ isCustom: true,
164
+ },
165
+ {
166
+ id: 'c4',
167
+ title: 'Original Mix 2026',
168
+ artist: 'Studio Session',
169
+ url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-14.mp3',
170
+ duration: '2:58',
171
+ cover: 'https://images.unsplash.com/photo-1514525253161-7a46d19cd819?w=150&auto=format&fit=crop&q=60',
172
+ isCustom: true,
173
+ }
174
+ ];
175
+ interface EditorStateSnapshot {
176
+ imageOptions: ImageEditOptions;
177
+ activeFilter: string;
178
+ trimStart: number;
179
+ trimEnd: number;
180
+ cropRatio: number | 'custom' | null;
181
+ cropOffset: { x: number; y: number };
182
+ zoomScale: number;
183
+ overlays: Array<{ id: string; text: string; x: number; y: number; color: string; fontSize: number }>;
184
+ stickers: Array<{ id: string; emoji: string; x: number; y: number; size: number }>;
185
+ activeEffect: string;
186
+ captions: Array<{ id: string; text: string; style: string; x: number; y: number }>;
187
+ }
188
+
189
+ export function EditorScreen({
190
+ items,
191
+ initialIndex = 0,
192
+ onBack,
193
+ onSaved,
194
+ onOpenCrop,
195
+ musicList,
196
+ }: {
197
+ items: MediaItem[];
198
+ initialIndex?: number;
199
+ onBack: () => void;
200
+ onSaved: (updatedItems: MediaItem[]) => void;
201
+ onOpenCrop: (item: MediaItem) => void;
202
+ musicList?: MusicTrack[];
203
+ }) {
204
+ const [activeIndex, setActiveIndex] = useState(initialIndex);
205
+ const currentItem = items[activeIndex] || items[0];
206
+ const item = currentItem; // Aliasing to 'item' for ease of compatibility
207
+
208
+ const [activeFilter, setActiveFilter] = useState('none');
209
+ const [imageOptions, setImageOptions] = useState<ImageEditOptions>({
210
+ rotateDegrees: 0,
211
+ flipX: false,
212
+ flipY: false,
213
+ brightness: 0,
214
+ contrast: 1,
215
+ saturation: 1,
216
+ grayscale: false,
217
+ });
218
+ const [trimStart, setTrimStart] = useState(0);
219
+ const [trimEnd, setTrimEnd] = useState(item.durationMs || 10000);
220
+
221
+ useEffect(() => {
222
+ setTrimStart(0);
223
+ setTrimEnd(item.durationMs || 10000);
224
+ setThumbnails([]);
225
+ }, [item.id, item.durationMs]);
226
+
227
+ const [editsHistory, setEditsHistory] = useState<Record<string, any>>({});
228
+ const editsHistoryRef = useRef<Record<string, any>>({});
229
+ const [dimensionsMap, setDimensionsMap] = useState<Record<string, { width: number; height: number }>>({});
230
+ const flatListRef = useRef<FlatList>(null);
231
+
232
+ useEffect(() => {
233
+ if (initialIndex > 0 && flatListRef.current) {
234
+ setTimeout(() => {
235
+ flatListRef.current?.scrollToIndex({ index: initialIndex, animated: false });
236
+ }, 150);
237
+ }
238
+ }, [initialIndex]);
239
+
240
+ // Auto-play video whenever active card changes in multi-select mode
241
+ useEffect(() => {
242
+ setVideoPaused(false);
243
+ }, [activeIndex]);
244
+
245
+ const saveEditsForIndex = (index: number) => {
246
+ const targetItem = items[index];
247
+ if (!targetItem) return;
248
+ const currentEdits = {
249
+ activeFilter,
250
+ imageOptions,
251
+ trimStart,
252
+ trimEnd,
253
+ overlays,
254
+ cropRatio,
255
+ cropOffset,
256
+ zoomScale,
257
+ straightenAngle,
258
+ isMuted,
259
+ };
260
+ editsHistoryRef.current[targetItem.id] = currentEdits;
261
+ setEditsHistory((prev) => ({
262
+ ...prev,
263
+ [targetItem.id]: currentEdits,
264
+ }));
265
+ };
266
+
267
+ const loadEditsForIndex = (index: number) => {
268
+ setUndoStack([]);
269
+ setRedoStack([]);
270
+ const targetItem = items[index];
271
+ if (!targetItem) return;
272
+ const saved = editsHistoryRef.current[targetItem.id];
273
+ if (saved) {
274
+ setActiveFilter(saved.activeFilter);
275
+ setImageOptions(saved.imageOptions);
276
+ setTrimStart(saved.trimStart);
277
+ setTrimEnd(saved.trimEnd);
278
+ setOverlays(saved.overlays);
279
+ setCropRatio(saved.cropRatio);
280
+ setCropOffset(saved.cropOffset);
281
+ setZoomScale(saved.zoomScale);
282
+ setStraightenAngle(saved.straightenAngle);
283
+ setIsMuted(saved.isMuted);
284
+ } else {
285
+ setActiveFilter('none');
286
+ setImageOptions({
287
+ rotateDegrees: 0,
288
+ flipX: false,
289
+ flipY: false,
290
+ brightness: 0,
291
+ contrast: 1,
292
+ saturation: 1,
293
+ grayscale: false,
294
+ });
295
+ setTrimStart(0);
296
+ setTrimEnd(targetItem.durationMs || 10000);
297
+ setOverlays([]);
298
+ setCropRatio(null);
299
+ setCropOffset({ x: 0, y: 0 });
300
+ setZoomScale(1);
301
+ setStraightenAngle(0);
302
+ setIsMuted(false);
303
+ }
304
+ };
305
+
306
+ const handleScrollEnd = (e: any) => {
307
+ const offsetX = e.nativeEvent.contentOffset.x;
308
+ const newIndex = Math.round(offsetX / SNAP_INTERVAL);
309
+ if (newIndex >= 0 && newIndex < items.length && newIndex !== activeIndex) {
310
+ saveEditsForIndex(activeIndex);
311
+ setActiveIndex(newIndex);
312
+ loadEditsForIndex(newIndex);
313
+ }
314
+ };
315
+
316
+ const buildOptionsForItem = (targetItem: MediaItem, edits: any) => {
317
+ const targetDim = dimensionsMap[targetItem.id] || { width: 1080, height: 1080 };
318
+ let imgW = targetDim.width;
319
+ let imgH = targetDim.height;
320
+ if ((edits.imageOptions.rotateDegrees || 0) % 180 !== 0) {
321
+ imgW = targetDim.height;
322
+ imgH = targetDim.width;
323
+ }
324
+
325
+ const scale = maxPan.scale;
326
+ const cropWidth = maxPan.boxW / scale;
327
+ const cropHeight = maxPan.boxH / scale;
328
+
329
+ const centerX = (imgW - cropWidth) / 2;
330
+ const centerY = (imgH - cropHeight) / 2;
331
+
332
+ let nx = centerX - (edits.cropOffset.x / scale);
333
+ let ny = centerY - (edits.cropOffset.y / scale);
334
+
335
+ nx = Math.max(0, Math.min(nx, imgW - cropWidth));
336
+ ny = Math.max(0, Math.min(ny, imgH - cropHeight));
337
+
338
+ const finalCrop = {
339
+ x: Math.round(nx),
340
+ y: Math.round(ny),
341
+ width: Math.round(cropWidth),
342
+ height: Math.round(cropHeight),
343
+ };
344
+
345
+ const frameConfig = (FRAME_CONFIGS as any)[edits.imageOptions.frame || ''] || { scale: 1, offsetY: 0 };
346
+ const renderScale = 1 / scale;
347
+
348
+ const hasCrop = edits.cropRatio !== null || edits.zoomScale > 1 || edits.cropOffset.x !== 0 || edits.cropOffset.y !== 0;
349
+
350
+ return {
351
+ ...edits.imageOptions,
352
+ rotateDegrees: (edits.imageOptions.rotateDegrees || 0) + edits.straightenAngle,
353
+ ...(hasCrop ? { crop: finalCrop } : {}),
354
+ imageAspectRatio: targetDim.width / (targetDim.height || 1),
355
+ frameScale: edits.imageOptions.frame ? frameConfig.scale : 1,
356
+ frameOffsetY: edits.imageOptions.frame ? (frameConfig.offsetY || 0) : 0,
357
+ overlays: edits.overlays.map((o: any) => ({
358
+ text: o.text,
359
+ x: (o.x + 8) * renderScale,
360
+ y: (o.y + 8) * renderScale,
361
+ color: o.color,
362
+ fontSize: o.fontSize * renderScale,
363
+ })),
364
+ };
365
+ };
366
+
367
+ const [saving, setSaving] = useState(false);
368
+ const [videoPaused, setVideoPaused] = useState(false);
369
+ const [panel, setPanel] = useState<'filter' | 'edit' | 'trim' | 'transform' | 'frame' | 'text' | 'ar' | 'music' | 'sticker' | 'effects' | 'caption' | 'addclip'>(item.type === 'video' ? 'trim' : 'filter');
370
+ const resolvedMusicList = musicList || DUMMY_MUSIC_LIST;
371
+
372
+ const [selectedMusic, setSelectedMusic] = useState<MusicTrack | null>(null);
373
+ const [musicPaused, setMusicPaused] = useState(false);
374
+ const [showMusicModal, setShowMusicModal] = useState(false);
375
+ const [musicSearchQuery, setMusicSearchQuery] = useState('');
376
+ const [activeMusicTab, setActiveMusicTab] = useState<'for_you' | 'trending' | 'saved' | 'original' | 'custom'>('trending');
377
+
378
+ const filteredMusicList = useMemo(() => {
379
+ let list = resolvedMusicList;
380
+ const hasCustomTracks = resolvedMusicList.some(track => track.isCustom);
381
+
382
+ if (musicList) {
383
+ // If custom musicList is passed as a prop, keep it clean
384
+ if (activeMusicTab === 'trending') {
385
+ list = hasCustomTracks ? resolvedMusicList.filter(track => !track.isCustom) : resolvedMusicList;
386
+ } else if (activeMusicTab === 'for_you') {
387
+ list = hasCustomTracks ? resolvedMusicList.filter(track => !track.isCustom) : resolvedMusicList;
388
+ } else if (activeMusicTab === 'saved') {
389
+ const nonCustom = hasCustomTracks ? resolvedMusicList.filter(track => !track.isCustom) : resolvedMusicList;
390
+ list = nonCustom.slice(0, Math.min(nonCustom.length, 3));
391
+ } else if (activeMusicTab === 'original') {
392
+ list = resolvedMusicList.filter(track => track.title.toLowerCase().includes('original') || track.artist.toLowerCase().includes('original'));
393
+ } else if (activeMusicTab === 'custom') {
394
+ list = hasCustomTracks ? resolvedMusicList.filter(track => track.isCustom) : resolvedMusicList;
395
+ }
396
+ } else {
397
+ // Fallback to DUMMY_MUSIC_LIST with mock slicing
398
+ if (activeMusicTab === 'trending') {
399
+ const nonCustom = resolvedMusicList.filter(track => !track.isCustom);
400
+ list = nonCustom.slice(4, 10);
401
+ } else if (activeMusicTab === 'for_you') {
402
+ const nonCustom = resolvedMusicList.filter(track => !track.isCustom);
403
+ list = nonCustom.slice(0, 4);
404
+ } else if (activeMusicTab === 'saved') {
405
+ const nonCustom = resolvedMusicList.filter(track => !track.isCustom);
406
+ list = [nonCustom[1], nonCustom[3], nonCustom[5]];
407
+ } else if (activeMusicTab === 'original') {
408
+ list = resolvedMusicList.filter(track => track.title.toLowerCase().includes('original') || track.artist.toLowerCase().includes('original') || track.id === '5');
409
+ } else if (activeMusicTab === 'custom') {
410
+ list = resolvedMusicList.filter(track => track.isCustom);
411
+ }
412
+ }
413
+
414
+ if (musicSearchQuery.trim().length > 0) {
415
+ const q = musicSearchQuery.toLowerCase();
416
+ list = list.filter(
417
+ (track) =>
418
+ track.title.toLowerCase().includes(q) ||
419
+ track.artist.toLowerCase().includes(q)
420
+ );
421
+ }
422
+ return list;
423
+ }, [activeMusicTab, musicSearchQuery, resolvedMusicList, musicList]);
424
+
425
+ const [thumbnails, setThumbnails] = useState<string[]>([]);
426
+ const [isMuted, setIsMuted] = useState(false);
427
+ const [isEditingVideo, setIsEditingVideo] = useState(false);
428
+ const [currentTimeMs, setCurrentTimeMs] = useState(0);
429
+ const [seekToMs, setSeekToMs] = useState<number>(-1);
430
+ const [scrollEnabled, setScrollEnabled] = useState(true);
431
+
432
+ const isUserScrolling = useRef(false);
433
+ const isDraggingHandle = useRef(false);
434
+ const timelineScrollRef = useRef<ScrollView>(null);
435
+
436
+ const isMomentumScrolling = useRef(false);
437
+ const lastSeekTime = useRef(0);
438
+ const pendingSeek = useRef<number | null>(null);
439
+ const seekTimeout = useRef<any>(null);
440
+
441
+ const throttledSeek = (time: number) => {
442
+ const now = Date.now();
443
+ if (now - lastSeekTime.current > 65) {
444
+ lastSeekTime.current = now;
445
+ setSeekToMs(time);
446
+ if (seekTimeout.current) {
447
+ clearTimeout(seekTimeout.current);
448
+ seekTimeout.current = null;
449
+ }
450
+ } else {
451
+ pendingSeek.current = time;
452
+ if (!seekTimeout.current) {
453
+ seekTimeout.current = setTimeout(() => {
454
+ if (pendingSeek.current !== null) {
455
+ setSeekToMs(pendingSeek.current);
456
+ lastSeekTime.current = Date.now();
457
+ pendingSeek.current = null;
458
+ }
459
+ seekTimeout.current = null;
460
+ }, 65);
461
+ }
462
+ }
463
+ };
464
+
465
+ const [cropRatio, setCropRatio] = useState<number | 'custom' | null>(null);
466
+ const [showQuickRatioMenu, setShowQuickRatioMenu] = useState(false);
467
+ const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
468
+ const [crop, setCrop] = useState<{ x: number; y: number; width: number; height: number } | null>(null);
469
+ const [transformBackup, setTransformBackup] = useState<any>(null);
470
+ const [overlays, setOverlays] = useState<Array<{ id: string; text: string; x: number; y: number; color: string; fontSize: number }>>([]);
471
+ const [editingTextId, setEditingTextId] = useState<string | null>(null);
472
+ const [newText, setNewText] = useState('');
473
+ const isNewOverlay = React.useRef(false); // track if overlay was just created (not yet saved)
474
+ const originalOverlayBackup = React.useRef<{ id: string; text: string; x: number; y: number; color: string; fontSize: number } | null>(null);
475
+
476
+ // ── Stickers ────────────────────────────────────────────────────────────────
477
+ const STICKER_LIST = [
478
+ '😂', '❤️', '🔥', '✨', '💯', '👑', '🎉', '🌈', '🎶', '💫',
479
+ '🙌', '😍', '🤩', '😎', '🥳', '🌸', '🦋', '⚡', '🌙', '💎',
480
+ '🍕', '🎸', '🎬', '📸', '🎯', '🌺', '🦄', '🐉', '🎭', '🪩',
481
+ ];
482
+ const [stickers, setStickers] = useState<Array<{ id: string; emoji: string; x: number; y: number; size: number }>>([]);
483
+ const addSticker = (emoji: string) => {
484
+ pushToHistory();
485
+ setStickers(prev => [...prev, { id: Date.now().toString(), emoji, x: 80, y: 80, size: 48 }]);
486
+ };
487
+ const removeSticker = (id: string) => {
488
+ pushToHistory();
489
+ setStickers(prev => prev.filter(s => s.id !== id));
490
+ };
491
+
492
+ // ── Effects ─────────────────────────────────────────────────────────────────
493
+ const EFFECTS_LIST = [
494
+ { id: 'none', label: 'None', icon: '⬜' },
495
+ { id: 'glitch', label: 'Glitch', icon: '📡' },
496
+ { id: 'blur', label: 'Blur', icon: '💨' },
497
+ { id: 'vignette', label: 'Vignette', icon: '🌑' },
498
+ { id: 'grain', label: 'Grain', icon: '📺' },
499
+ { id: 'retro', label: 'Retro', icon: '📷' },
500
+ { id: 'neon', label: 'Neon', icon: '💡' },
501
+ { id: 'fade', label: 'Fade', icon: '🌫️' },
502
+ ];
503
+ const [activeEffect, setActiveEffect] = useState('none');
504
+
505
+ // ── Captions ─────────────────────────────────────────────────────────────────
506
+ const CAPTION_STYLES = [
507
+ { id: 'classic', label: 'Classic', bg: 'rgba(0,0,0,0.6)', color: '#fff', fontWeight: '400' as const },
508
+ { id: 'bold', label: 'Bold', bg: 'rgba(0,0,0,0.8)', color: '#FFD700', fontWeight: '900' as const },
509
+ { id: 'neon', label: 'Neon', bg: 'rgba(0,0,80,0.7)', color: '#00FFFF', fontWeight: '700' as const },
510
+ { id: 'minimal', label: 'Minimal', bg: 'transparent', color: '#fff', fontWeight: '300' as const },
511
+ { id: 'warm', label: 'Warm', bg: 'rgba(180,80,0,0.7)', color: '#FFF8DC', fontWeight: '600' as const },
512
+ ];
513
+ const [captions, setCaptions] = useState<Array<{ id: string; text: string; style: string; x: number; y: number }>>([]);
514
+ const [captionInput, setCaptionInput] = useState('');
515
+ const [captionStyle, setCaptionStyle] = useState('classic');
516
+ const addCaption = () => {
517
+ if (!captionInput.trim()) return;
518
+ pushToHistory();
519
+ setCaptions(prev => [...prev, { id: Date.now().toString(), text: captionInput.trim(), style: captionStyle, x: 60, y: 200 }]);
520
+ setCaptionInput('');
521
+ };
522
+ const removeCaption = (id: string) => {
523
+ pushToHistory();
524
+ setCaptions(prev => prev.filter(c => c.id !== id));
525
+ };
526
+
527
+ const addTextOverlay = () => {
528
+ pushToHistory();
529
+ const id = Date.now().toString();
530
+ const newItem = {
531
+ id,
532
+ text: '', // start empty so placeholder shows
533
+ x: 50,
534
+ y: 50,
535
+ color: '#FFFFFF',
536
+ fontSize: 24,
537
+ };
538
+ // Add immediately so live typing & coloring works!
539
+ setOverlays(prev => [...prev, newItem]);
540
+ setEditingTextId(id);
541
+ setNewText('');
542
+ isNewOverlay.current = true;
543
+ originalOverlayBackup.current = null;
544
+ setPanel('text');
545
+ };
546
+
547
+ const updateTextOverlay = (id: string, patch: any) => {
548
+ setOverlays(prev => prev.map(o => o.id === id ? { ...o, ...patch } : o));
549
+ };
550
+
551
+ const removeTextOverlay = (id: string) => {
552
+ pushToHistory();
553
+ setOverlays(prev => prev.filter(o => o.id !== id));
554
+ if (editingTextId === id) setEditingTextId(null);
555
+ };
556
+
557
+ const handleOpenTransform = () => {
558
+ // Open the dedicated CropScreen
559
+ onOpenCrop(item);
560
+ };
561
+
562
+ const handleCancelTransform = () => {
563
+ if (transformBackup) {
564
+ setImageOptions((prev) => ({
565
+ ...prev,
566
+ rotateDegrees: transformBackup.rotateDegrees,
567
+ flipX: transformBackup.flipX,
568
+ flipY: transformBackup.flipY,
569
+ }));
570
+ setCropRatio(transformBackup.cropRatio);
571
+ setCrop(transformBackup.crop);
572
+ setZoomScale(transformBackup.zoomScale ?? 1);
573
+ setCropOffset(transformBackup.cropOffset ?? { x: 0, y: 0 });
574
+ setStraightenAngle(transformBackup.straightenAngle ?? 0);
575
+ setCropResizeScale(transformBackup.cropResizeScale ?? 1);
576
+ }
577
+ setPanel('filter');
578
+ };
579
+
580
+ useEffect(() => {
581
+ let cancelled = false;
582
+
583
+ if (item.width && item.height) {
584
+ setDimensions({ width: item.width, height: item.height });
585
+ setDimensionsMap((prev) => ({ ...prev, [item.id]: { width: item.width || 0, height: item.height || 0 } }));
586
+ return () => {
587
+ cancelled = true;
588
+ };
589
+ }
590
+
591
+ if (item.type === 'image') {
592
+ Image.getSize(
593
+ item.uri,
594
+ (width, height) => {
595
+ if (!cancelled) {
596
+ setDimensions({ width, height });
597
+ setDimensionsMap((prev) => ({ ...prev, [item.id]: { width, height } }));
598
+ }
599
+ },
600
+ (err) => {
601
+ console.warn('Failed to get image size, using fallback', err);
602
+ if (!cancelled) {
603
+ setDimensions({ width: 1080, height: 1080 });
604
+ setDimensionsMap((prev) => ({ ...prev, [item.id]: { width: 1080, height: 1080 } }));
605
+ }
606
+ }
607
+ );
608
+ return () => {
609
+ cancelled = true;
610
+ };
611
+ }
612
+
613
+ // For videos, resolve accurate dimensions from a captured frame.
614
+ // This keeps exported crop/portrait/square changes aligned with preview.
615
+ (async () => {
616
+ try {
617
+ const frameUri = await captureFrame(item.uri, { timeMs: 0 });
618
+ Image.getSize(
619
+ frameUri,
620
+ (width, height) => {
621
+ if (!cancelled) {
622
+ setDimensions({ width, height });
623
+ setDimensionsMap((prev) => ({ ...prev, [item.id]: { width, height } }));
624
+ }
625
+ },
626
+ () => {
627
+ if (!cancelled) {
628
+ setDimensions({ width: 1080, height: 1920 });
629
+ setDimensionsMap((prev) => ({ ...prev, [item.id]: { width: 1080, height: 1920 } }));
630
+ }
631
+ }
632
+ );
633
+ } catch {
634
+ if (!cancelled) {
635
+ setDimensions({ width: 1080, height: 1920 });
636
+ setDimensionsMap((prev) => ({ ...prev, [item.id]: { width: 1080, height: 1920 } }));
637
+ }
638
+ }
639
+ })();
640
+
641
+ return () => {
642
+ cancelled = true;
643
+ };
644
+ }, [item.uri, item.width, item.height, item.type, item.id]);
645
+
646
+ const [cropOffset, setCropOffset] = useState({ x: 0, y: 0 });
647
+ const [zoomScale, setZoomScale] = useState(1);
648
+ const [straightenAngle, setStraightenAngle] = useState(0);
649
+ const [cropResizeScale, setCropResizeScale] = useState(1);
650
+ const [cropResizeScaleX, setCropResizeScaleX] = useState(1);
651
+ const [cropResizeScaleY, setCropResizeScaleY] = useState(1);
652
+ const panStart = useRef({ x: 0, y: 0 });
653
+ const lastTouchDist = useRef<number | null>(null);
654
+ const resizeStartScale = useRef(1);
655
+ const resizeStartScaleX = useRef(1);
656
+ const resizeStartScaleY = useRef(1);
657
+ const cropRatioRef = useRef<number | 'custom' | null>(cropRatio);
658
+ const panelRef = useRef(panel);
659
+ const itemTypeRef = useRef(item.type);
660
+ const cropOffsetRef = useRef(cropOffset);
661
+ const zoomScaleRef = useRef(zoomScale);
662
+
663
+ const [undoStack, setUndoStack] = useState<EditorStateSnapshot[]>([]);
664
+ const [redoStack, setRedoStack] = useState<EditorStateSnapshot[]>([]);
665
+
666
+ const pushToHistory = () => {
667
+ const snapshot: EditorStateSnapshot = {
668
+ imageOptions: { ...imageOptions },
669
+ activeFilter,
670
+ trimStart,
671
+ trimEnd,
672
+ cropRatio,
673
+ cropOffset: { ...cropOffset },
674
+ zoomScale,
675
+ overlays: overlays.map(o => ({ ...o })),
676
+ stickers: stickers.map(s => ({ ...s })),
677
+ activeEffect,
678
+ captions: captions.map(c => ({ ...c })),
679
+ };
680
+ setUndoStack(prev => [...prev, snapshot]);
681
+ setRedoStack([]); // Clear redo stack on new action
682
+ };
683
+
684
+ const applySnapshot = (snapshot: EditorStateSnapshot) => {
685
+ setImageOptions(snapshot.imageOptions);
686
+ setActiveFilter(snapshot.activeFilter);
687
+ setTrimStart(snapshot.trimStart);
688
+ setTrimEnd(snapshot.trimEnd);
689
+ setCropRatio(snapshot.cropRatio);
690
+ setCropOffset(snapshot.cropOffset);
691
+ setZoomScale(snapshot.zoomScale);
692
+ setOverlays(snapshot.overlays);
693
+ setStickers(snapshot.stickers);
694
+ setActiveEffect(snapshot.activeEffect);
695
+ setCaptions(snapshot.captions);
696
+
697
+ // If trim changes, seek to trimStart
698
+ if (snapshot.trimStart !== trimStart) {
699
+ throttledSeek(snapshot.trimStart);
700
+ }
701
+ };
702
+
703
+ const handleUndo = () => {
704
+ if (undoStack.length === 0) return;
705
+
706
+ const currentSnapshot: EditorStateSnapshot = {
707
+ imageOptions: { ...imageOptions },
708
+ activeFilter,
709
+ trimStart,
710
+ trimEnd,
711
+ cropRatio,
712
+ cropOffset: { ...cropOffset },
713
+ zoomScale,
714
+ overlays: overlays.map(o => ({ ...o })),
715
+ stickers: stickers.map(s => ({ ...s })),
716
+ activeEffect,
717
+ captions: captions.map(c => ({ ...c })),
718
+ };
719
+
720
+ const previousSnapshot = undoStack[undoStack.length - 1];
721
+ setUndoStack(prev => prev.slice(0, prev.length - 1));
722
+ setRedoStack(prev => [...prev, currentSnapshot]);
723
+
724
+ applySnapshot(previousSnapshot);
725
+ };
726
+
727
+ const handleRedo = () => {
728
+ if (redoStack.length === 0) return;
729
+
730
+ const currentSnapshot: EditorStateSnapshot = {
731
+ imageOptions: { ...imageOptions },
732
+ activeFilter,
733
+ trimStart,
734
+ trimEnd,
735
+ cropRatio,
736
+ cropOffset: { ...cropOffset },
737
+ zoomScale,
738
+ overlays: overlays.map(o => ({ ...o })),
739
+ stickers: stickers.map(s => ({ ...s })),
740
+ activeEffect,
741
+ captions: captions.map(c => ({ ...c })),
742
+ };
743
+
744
+ const nextSnapshot = redoStack[redoStack.length - 1];
745
+ setRedoStack(prev => prev.slice(0, prev.length - 1));
746
+ setUndoStack(prev => [...prev, currentSnapshot]);
747
+
748
+ applySnapshot(nextSnapshot);
749
+ };
750
+
751
+ useEffect(() => {
752
+ cropRatioRef.current = cropRatio;
753
+ panelRef.current = panel;
754
+ itemTypeRef.current = item.type;
755
+ cropOffsetRef.current = cropOffset;
756
+ zoomScaleRef.current = zoomScale;
757
+ }, [cropRatio, panel, item.type, cropOffset, zoomScale]);
758
+
759
+ const handleStraighten = (gesture: any) => {
760
+ // Simple straighten logic: 1 pixel = 0.2 degrees
761
+ const delta = gesture.dx * 0.2;
762
+ setStraightenAngle(prev => Math.max(-45, Math.min(45, prev + delta)));
763
+ };
764
+
765
+ const straightenPan = useRef(
766
+ PanResponder.create({
767
+ onStartShouldSetPanResponder: () => true,
768
+ onPanResponderMove: (_, gesture) => {
769
+ const delta = gesture.dx * 0.1;
770
+ setStraightenAngle(prev => Math.max(-45, Math.min(45, prev + delta)));
771
+ },
772
+ })
773
+ ).current;
774
+
775
+ const createCornerPan = (corner: 'TL' | 'TR' | 'BL' | 'BR') => {
776
+ return PanResponder.create({
777
+ onStartShouldSetPanResponder: () => true,
778
+ onPanResponderGrant: () => {
779
+ resizeStartScale.current = cropResizeScale;
780
+ resizeStartScaleX.current = cropResizeScaleX;
781
+ resizeStartScaleY.current = cropResizeScaleY;
782
+ },
783
+ onPanResponderMove: (_, gesture) => {
784
+ if (cropRatioRef.current === 'custom') {
785
+ const sensitivity = 0.005;
786
+ let xDelta = 0;
787
+ let yDelta = 0;
788
+ if (corner === 'TL') {
789
+ xDelta = -gesture.dx;
790
+ yDelta = -gesture.dy;
791
+ } else if (corner === 'TR') {
792
+ xDelta = gesture.dx;
793
+ yDelta = -gesture.dy;
794
+ } else if (corner === 'BL') {
795
+ xDelta = -gesture.dx;
796
+ yDelta = gesture.dy;
797
+ } else {
798
+ xDelta = gesture.dx;
799
+ yDelta = gesture.dy;
800
+ }
801
+ const newScaleX = Math.max(0.2, Math.min(1.0, resizeStartScaleX.current + xDelta * sensitivity));
802
+ const newScaleY = Math.max(0.2, Math.min(1.0, resizeStartScaleY.current + yDelta * sensitivity));
803
+ setCropResizeScaleX(newScaleX);
804
+ setCropResizeScaleY(newScaleY);
805
+ return;
806
+ }
807
+
808
+ let delta = 0;
809
+ const sensitivity = 0.005;
810
+ if (corner === 'TL') delta = -gesture.dx - gesture.dy;
811
+ else if (corner === 'TR') delta = gesture.dx - gesture.dy;
812
+ else if (corner === 'BL') delta = -gesture.dx + gesture.dy;
813
+ else if (corner === 'BR') delta = gesture.dx + gesture.dy;
814
+
815
+ const newScale = Math.max(0.3, Math.min(1.0, resizeStartScale.current + delta * sensitivity));
816
+ setCropResizeScale(newScale);
817
+ }
818
+ });
819
+ };
820
+
821
+ const cornerTL = useRef(createCornerPan('TL')).current;
822
+ const cornerTR = useRef(createCornerPan('TR')).current;
823
+ const cornerBL = useRef(createCornerPan('BL')).current;
824
+ const cornerBR = useRef(createCornerPan('BR')).current;
825
+
826
+ const createTextPan = (id: string) => {
827
+ let startX = 0;
828
+ let startY = 0;
829
+ return PanResponder.create({
830
+ onStartShouldSetPanResponder: () => true,
831
+ onMoveShouldSetPanResponder: () => true,
832
+ onPanResponderGrant: () => {
833
+ setOverlays(prev => {
834
+ const item = prev.find(o => o.id === id);
835
+ if (item) {
836
+ startX = item.x;
837
+ startY = item.y;
838
+ }
839
+ return prev;
840
+ });
841
+ },
842
+ onPanResponderMove: (_, gesture) => {
843
+ setOverlays(prev => prev.map(o => o.id === id ? {
844
+ ...o,
845
+ x: startX + gesture.dx,
846
+ y: startY + gesture.dy
847
+ } : o));
848
+ }
849
+ });
850
+ };
851
+
852
+ const textResponders = useRef<Record<string, any>>({});
853
+
854
+ const getTextResponder = (id: string) => {
855
+ if (!textResponders.current[id]) {
856
+ textResponders.current[id] = createTextPan(id);
857
+ }
858
+ return textResponders.current[id];
859
+ };
860
+
861
+ const containerHeight = showMusicModal ? (SCREEN_WIDTH * 0.72) : (SCREEN_WIDTH * 1.25);
862
+
863
+ const maxPan = useMemo(() => {
864
+ // If no cropRatio, we still want to allow panning/zooming if zoomScale > 1
865
+ const actualRatio = typeof cropRatio === 'number' ? cropRatio : dimensions.width / (dimensions.height || 1);
866
+
867
+ let imgW = dimensions.width;
868
+ let imgH = dimensions.height;
869
+ if ((imageOptions.rotateDegrees || 0) % 180 !== 0) {
870
+ imgW = dimensions.height;
871
+ imgH = dimensions.width;
872
+ }
873
+
874
+ const baseH = actualRatio <= 1 ? containerHeight - 24 : (SCREEN_WIDTH - 24) / actualRatio;
875
+ const baseW = actualRatio > 1 ? SCREEN_WIDTH - 24 : baseH * actualRatio;
876
+
877
+ const boxW = baseW * (cropRatio === 'custom' ? cropResizeScaleX : cropResizeScale);
878
+ const boxH = baseH * (cropRatio === 'custom' ? cropResizeScaleY : cropResizeScale);
879
+
880
+ const minScale = Math.max(boxW / (imgW || 1), boxH / (imgH || 1));
881
+ const totalScale = minScale * zoomScale;
882
+
883
+ return {
884
+ dx: Math.max(0, ((imgW * totalScale) - boxW) / 2),
885
+ dy: Math.max(0, ((imgH * totalScale) - boxH) / 2),
886
+ scale: totalScale,
887
+ boxW,
888
+ boxH
889
+ };
890
+ }, [cropRatio, dimensions, imageOptions.rotateDegrees, zoomScale, cropResizeScale, cropResizeScaleX, cropResizeScaleY, showMusicModal, containerHeight]);
891
+
892
+ const maxPanRef = useRef(maxPan);
893
+ useEffect(() => {
894
+ maxPanRef.current = maxPan;
895
+ }, [maxPan]);
896
+
897
+ const cropPan = useRef(
898
+ PanResponder.create({
899
+ onStartShouldSetPanResponder: () => panelRef.current === 'transform',
900
+ onMoveShouldSetPanResponder: () => panelRef.current === 'transform',
901
+ onPanResponderGrant: (evt) => {
902
+ pushToHistory();
903
+ panStart.current = { x: cropOffsetRef.current.x, y: cropOffsetRef.current.y };
904
+ if (evt.nativeEvent.touches.length === 2) {
905
+ const t1 = evt.nativeEvent.touches[0];
906
+ const t2 = evt.nativeEvent.touches[1];
907
+ lastTouchDist.current = Math.sqrt(
908
+ Math.pow(t2.pageX - t1.pageX, 2) + Math.pow(t2.pageY - t1.pageY, 2)
909
+ );
910
+ } else {
911
+ lastTouchDist.current = null;
912
+ }
913
+ },
914
+ onPanResponderMove: (evt, gesture) => {
915
+ if (evt.nativeEvent.touches.length === 2 && lastTouchDist.current !== null) {
916
+ // Handle Pinch-to-Zoom
917
+ const t1 = evt.nativeEvent.touches[0];
918
+ const t2 = evt.nativeEvent.touches[1];
919
+ const dist = Math.sqrt(
920
+ Math.pow(t2.pageX - t1.pageX, 2) + Math.pow(t2.pageY - t1.pageY, 2)
921
+ );
922
+ const delta = dist / lastTouchDist.current;
923
+ setZoomScale(prev => Math.max(1, Math.min(prev * delta, 5)));
924
+ lastTouchDist.current = dist;
925
+ } else if (evt.nativeEvent.touches.length === 1) {
926
+ // Handle Pan
927
+ const nx = Math.max(-maxPanRef.current.dx, Math.min(panStart.current.x + gesture.dx, maxPanRef.current.dx));
928
+ const ny = Math.max(-maxPanRef.current.dy, Math.min(panStart.current.y + gesture.dy, maxPanRef.current.dy));
929
+ setCropOffset({ x: nx, y: ny });
930
+ }
931
+ },
932
+ onPanResponderRelease: () => {
933
+ lastTouchDist.current = null;
934
+ }
935
+ })
936
+ ).current;
937
+
938
+ const activeOptions = useMemo(() => {
939
+ let imgW = dimensions.width;
940
+ let imgH = dimensions.height;
941
+ if ((imageOptions.rotateDegrees || 0) % 180 !== 0) {
942
+ imgW = dimensions.height;
943
+ imgH = dimensions.width;
944
+ }
945
+
946
+ const actualRatio = typeof cropRatio === 'number' ? cropRatio : imgW / (imgH || 1);
947
+ const scale = maxPan.scale;
948
+ const cropWidth = maxPan.boxW / scale;
949
+ const cropHeight = maxPan.boxH / scale;
950
+
951
+ const centerX = (imgW - cropWidth) / 2;
952
+ const centerY = (imgH - cropHeight) / 2;
953
+
954
+ let nx = centerX - (cropOffset.x / scale);
955
+ let ny = centerY - (cropOffset.y / scale);
956
+
957
+ nx = Math.max(0, Math.min(nx, imgW - cropWidth));
958
+ ny = Math.max(0, Math.min(ny, imgH - cropHeight));
959
+
960
+ const finalCrop = {
961
+ x: Math.round(nx),
962
+ y: Math.round(ny),
963
+ width: Math.round(cropWidth),
964
+ height: Math.round(cropHeight),
965
+ };
966
+
967
+ const frameConfig = (FRAME_CONFIGS as any)[imageOptions.frame || ''] || { scale: 1, offsetY: 0 };
968
+
969
+ // Scale factor to map screen points to actual image pixels
970
+ const renderScale = 1 / scale;
971
+
972
+ const hasCrop = cropRatio !== null || zoomScale > 1 || cropOffset.x !== 0 || cropOffset.y !== 0;
973
+
974
+ return {
975
+ ...imageOptions,
976
+ rotateDegrees: (imageOptions.rotateDegrees || 0) + straightenAngle,
977
+ ...(hasCrop ? { crop: finalCrop } : {}),
978
+ imageAspectRatio: dimensions.width / (dimensions.height || 1),
979
+ frameScale: imageOptions.frame ? frameConfig.scale : 1,
980
+ frameOffsetY: imageOptions.frame ? (frameConfig.offsetY || 0) : 0,
981
+ overlays: overlays.map(o => ({
982
+ text: o.text,
983
+ // Add 8px padding to match the visual position in the container
984
+ x: (o.x + 8) * renderScale,
985
+ y: (o.y + 8) * renderScale,
986
+ color: o.color,
987
+ fontSize: o.fontSize * renderScale,
988
+ })),
989
+ };
990
+ }, [imageOptions, cropOffset, maxPan, dimensions, cropRatio, straightenAngle, overlays]);
991
+
992
+ // For visual trim
993
+
994
+ const duration = item.durationMs ?? 10_000;
995
+
996
+ const formatTime = (ms: number) => {
997
+ const totalSec = Math.floor(ms / 1000);
998
+ const mins = Math.floor(totalSec / 60);
999
+ const secs = totalSec % 60;
1000
+ return `${mins}:${secs.toString().padStart(2, '0')}`;
1001
+ };
1002
+
1003
+ const handleTimelineScroll = (e: any) => {
1004
+ if (isUserScrolling.current) {
1005
+ const x = e.nativeEvent.contentOffset.x;
1006
+ const time = (x / TIMELINE_WIDTH) * duration;
1007
+ const clampedTime = Math.max(trimStart, Math.min(trimEnd, time));
1008
+ setCurrentTimeMs(clampedTime);
1009
+ throttledSeek(clampedTime);
1010
+ }
1011
+ };
1012
+
1013
+ useEffect(() => {
1014
+ if (videoPaused || item.type !== 'video' || !isEditingVideo) return;
1015
+ let lastTime = Date.now();
1016
+ const interval = setInterval(() => {
1017
+ const now = Date.now();
1018
+ const delta = now - lastTime;
1019
+ lastTime = now;
1020
+ setCurrentTimeMs((prev) => {
1021
+ let next = prev + delta;
1022
+ if (next >= duration) {
1023
+ next = 0;
1024
+ }
1025
+ return next;
1026
+ });
1027
+ }, 100);
1028
+ return () => clearInterval(interval);
1029
+ }, [videoPaused, duration, item.type, isEditingVideo]);
1030
+
1031
+ const startX = useRef((trimStart / duration) * TIMELINE_WIDTH);
1032
+ const endX = useRef((trimEnd / duration) * TIMELINE_WIDTH);
1033
+
1034
+ useEffect(() => {
1035
+ startX.current = (trimStart / duration) * TIMELINE_WIDTH;
1036
+ }, [trimStart, duration]);
1037
+
1038
+ useEffect(() => {
1039
+ endX.current = (trimEnd / duration) * TIMELINE_WIDTH;
1040
+ }, [trimEnd, duration]);
1041
+
1042
+ useEffect(() => {
1043
+ if (item.type === 'video' && thumbnails.length === 0) {
1044
+ const generateThumbs = async () => {
1045
+ const count = 10;
1046
+ const uris = [];
1047
+ for (let i = 0; i < count; i++) {
1048
+ const time = (i / (count - 1)) * duration;
1049
+ try {
1050
+ const uri = await captureFrame(item.uri, { timeMs: time });
1051
+ uris.push(uri);
1052
+ } catch (e) {
1053
+ console.error('Thumb extraction error', e);
1054
+ }
1055
+ }
1056
+ setThumbnails(uris);
1057
+ };
1058
+ generateThumbs();
1059
+ }
1060
+ }, [item.type, item.uri, duration, thumbnails.length]);
1061
+
1062
+ const startPanOffset = useRef(0);
1063
+ const startPan = useRef(
1064
+ PanResponder.create({
1065
+ onStartShouldSetPanResponder: () => true,
1066
+ onStartShouldSetPanResponderCapture: () => true,
1067
+ onMoveShouldSetPanResponder: () => true,
1068
+ onMoveShouldSetPanResponderCapture: () => true,
1069
+ onPanResponderGrant: () => {
1070
+ pushToHistory();
1071
+ startPanOffset.current = startX.current;
1072
+ isDraggingHandle.current = true;
1073
+ setScrollEnabled(false);
1074
+ },
1075
+ onPanResponderMove: (_, gesture) => {
1076
+ const newX = Math.max(0, Math.min(endX.current - 32, startPanOffset.current + gesture.dx));
1077
+ startX.current = newX;
1078
+ const newTime = (newX / TIMELINE_WIDTH) * duration;
1079
+ setTrimStart(newTime);
1080
+ throttledSeek(newTime);
1081
+ },
1082
+ onPanResponderRelease: () => {
1083
+ isDraggingHandle.current = false;
1084
+ setScrollEnabled(true);
1085
+ setSeekToMs(-1);
1086
+ },
1087
+ onPanResponderTerminate: () => {
1088
+ isDraggingHandle.current = false;
1089
+ setScrollEnabled(true);
1090
+ setSeekToMs(-1);
1091
+ }
1092
+ })
1093
+ ).current;
1094
+
1095
+ const endPanOffset = useRef(0);
1096
+ const endPan = useRef(
1097
+ PanResponder.create({
1098
+ onStartShouldSetPanResponder: () => true,
1099
+ onStartShouldSetPanResponderCapture: () => true,
1100
+ onMoveShouldSetPanResponder: () => true,
1101
+ onMoveShouldSetPanResponderCapture: () => true,
1102
+ onPanResponderGrant: () => {
1103
+ pushToHistory();
1104
+ endPanOffset.current = endX.current;
1105
+ isDraggingHandle.current = true;
1106
+ setScrollEnabled(false);
1107
+ },
1108
+ onPanResponderMove: (_, gesture) => {
1109
+ const newX = Math.min(TIMELINE_WIDTH, Math.max(startX.current + 32, endPanOffset.current + gesture.dx));
1110
+ endX.current = newX;
1111
+ const newTime = (newX / TIMELINE_WIDTH) * duration;
1112
+ setTrimEnd(newTime);
1113
+ throttledSeek(newTime);
1114
+ },
1115
+ onPanResponderRelease: () => {
1116
+ isDraggingHandle.current = false;
1117
+ setScrollEnabled(true);
1118
+ setSeekToMs(-1);
1119
+ },
1120
+ onPanResponderTerminate: () => {
1121
+ isDraggingHandle.current = false;
1122
+ setScrollEnabled(true);
1123
+ setSeekToMs(-1);
1124
+ }
1125
+ })
1126
+ ).current;
1127
+ // Instagram‑style filter presets – extended
1128
+ const FILTERS = {
1129
+ // Classics
1130
+ none: { label: 'Normal', brightness: 0, contrast: 1, saturation: 1, grayscale: false },
1131
+ clarendon: { label: 'Clarendon', brightness: 0.1, contrast: 1.2, saturation: 1.3, grayscale: false },
1132
+ gingham: { label: 'Gingham', brightness: 0.05, contrast: 0.9, saturation: 0.8, grayscale: false },
1133
+ moon: { label: 'Moon', brightness: 0.05, contrast: 1.1, saturation: 0, grayscale: true },
1134
+ lark: { label: 'Lark', brightness: 0.08, contrast: 1.15, saturation: 1.1, grayscale: false },
1135
+ reyes: { label: 'Reyes', brightness: 0.15, contrast: 0.85, saturation: 0.75, grayscale: false },
1136
+ juno: { label: 'Juno', brightness: 0.07, contrast: 1.15, saturation: 1.3, grayscale: false },
1137
+ slumber: { label: 'Slumber', brightness: -0.05, contrast: 0.9, saturation: 0.85, grayscale: false },
1138
+ crema: { label: 'Crema', brightness: 0.05, contrast: 0.95, saturation: 0.9, grayscale: false },
1139
+ ludwig: { label: 'Ludwig', brightness: 0.05, contrast: 1.1, saturation: 1.2, grayscale: false },
1140
+ aden: { label: 'Aden', brightness: 0.1, contrast: 0.9, saturation: 0.8, grayscale: false },
1141
+ perpetua: { label: 'Perpetua', brightness: 0.05, contrast: 1.05, saturation: 1.15, grayscale: false },
1142
+ amaro: { label: 'Amaro', brightness: 0.1, contrast: 1.1, saturation: 1.1, grayscale: false },
1143
+ mayfair: { label: 'Mayfair', brightness: 0.06, contrast: 1.1, saturation: 1.2, grayscale: false },
1144
+ rise: { label: 'Rise', brightness: 0.09, contrast: 1.05, saturation: 1.1, grayscale: false },
1145
+ valencia: { label: 'Valencia', brightness: 0.02, contrast: 1.05, saturation: 1.0, grayscale: false },
1146
+ xpro2: { label: 'X-Pro II', brightness: 0.03, contrast: 1.3, saturation: 1.25, grayscale: false },
1147
+ sierra: { label: 'Sierra', brightness: 0.05, contrast: 0.9, saturation: 0.9, grayscale: false },
1148
+ willow: { label: 'Willow', brightness: 0.05, contrast: 0.85, saturation: 0, grayscale: true },
1149
+ lofi: { label: 'Lo-Fi', brightness: 0.02, contrast: 1.4, saturation: 1.5, grayscale: false },
1150
+ inkwell: { label: 'Inkwell', brightness: 0.02, contrast: 1.3, saturation: 0, grayscale: true },
1151
+ nashville: { label: 'Nashville', brightness: 0.05, contrast: 1.1, saturation: 1.0, grayscale: false },
1152
+
1153
+ // Cities
1154
+ rio: { label: 'Rio de Janeiro', brightness: 0.08, contrast: 1.2, saturation: 1.4, grayscale: false },
1155
+ tokyo: { label: 'Tokyo', brightness: -0.02, contrast: 1.1, saturation: 1.0, grayscale: false },
1156
+ cairo: { label: 'Cairo', brightness: 0.05, contrast: 1.0, saturation: 1.1, grayscale: false },
1157
+ jaipur: { label: 'Jaipur', brightness: 0.07, contrast: 1.05, saturation: 1.2, grayscale: false },
1158
+ newyork: { label: 'New York', brightness: 0.02, contrast: 1.3, saturation: 0.9, grayscale: false },
1159
+ buenosaires: { label: 'Buenos Aires', brightness: 0.04, contrast: 1.1, saturation: 1.15, grayscale: false },
1160
+ abudhabi: { label: 'Abu Dhabi', brightness: 0.06, contrast: 1.05, saturation: 1.1, grayscale: false },
1161
+ jakarta: { label: 'Jakarta', brightness: 0.03, contrast: 1.15, saturation: 1.25, grayscale: false },
1162
+ melbourne: { label: 'Melbourne', brightness: 0.05, contrast: 1.0, saturation: 1.05, grayscale: false },
1163
+ oslo: { label: 'Oslo', brightness: -0.05, contrast: 1.1, saturation: 0.9, grayscale: false },
1164
+ la: { label: 'Los Angeles', brightness: 0.1, contrast: 1.1, saturation: 1.3, grayscale: false },
1165
+ paris: { label: 'Paris', brightness: 0.08, contrast: 0.95, saturation: 1.1, grayscale: false },
1166
+
1167
+ // Special & Effects
1168
+ wideangle: { label: 'Wide Angle', brightness: 0.05, contrast: 1.1, saturation: 1.1, grayscale: false, effect: 'vignette' },
1169
+ wavy: { label: 'Wavy', brightness: 0.02, contrast: 1.05, saturation: 1.2, grayscale: false, effect: 'vignette' },
1170
+ lores: { label: 'LO-Res', brightness: 0.05, contrast: 1.1, saturation: 1.15, grayscale: false, effect: 'pixelize' },
1171
+ moire: { label: 'MOire', brightness: 0, contrast: 1.1, saturation: 1.0, grayscale: false, effect: 'grain' },
1172
+ handheld: { label: 'Handheld', brightness: 0, contrast: 1.0, saturation: 1.0, grayscale: false, effect: 'vignette' },
1173
+ zoomblur: { label: 'Zoom Blur', brightness: 0.05, contrast: 1.15, saturation: 1.2, grayscale: false, effect: 'vignette' },
1174
+ softlight: { label: 'Soft Light', brightness: 0.1, contrast: 0.9, saturation: 1.0, grayscale: false },
1175
+ colorleak: { label: 'Color Leak', brightness: 0.05, contrast: 1.1, saturation: 1.2, grayscale: false },
1176
+ halo: { label: 'Halo', brightness: 0.15, contrast: 1.05, saturation: 1.1, grayscale: false },
1177
+ gritty: { label: 'Gritty', brightness: -0.05, contrast: 1.5, saturation: 0.8, grayscale: false, effect: 'grain' },
1178
+ grainy: { label: 'Grainy', brightness: 0, contrast: 1.2, saturation: 1.0, grayscale: false, effect: 'grain' },
1179
+ midnight: { label: 'Midnight', brightness: -0.15, contrast: 1.3, saturation: 0.7, grayscale: false },
1180
+ emerald: { label: 'Emerald', brightness: 0, contrast: 1.1, saturation: 1.2, grayscale: false },
1181
+ rosy: { label: 'Rosy', brightness: 0.05, contrast: 1.0, saturation: 1.2, grayscale: false },
1182
+ hyper: { label: 'Hyper', brightness: 0.05, contrast: 1.5, saturation: 2.0, grayscale: false },
1183
+ graphite: { label: 'Graphite', brightness: -0.05, contrast: 1.4, saturation: 0, grayscale: true },
1184
+
1185
+ // Boosts & Fades
1186
+ boostcool: { label: 'Boost Cool', brightness: 0.02, contrast: 1.1, saturation: 1.2, grayscale: false },
1187
+ boostwarm: { label: 'Boost Warm', brightness: 0.02, contrast: 1.1, saturation: 1.2, grayscale: false },
1188
+ boost: { label: 'Boost', brightness: 0.05, contrast: 1.2, saturation: 1.3, grayscale: false },
1189
+ simplecool: { label: 'Simple Cool', brightness: 0.02, contrast: 1.0, saturation: 1.1, grayscale: false },
1190
+ simplewarm: { label: 'Simple Warm', brightness: 0.02, contrast: 1.0, saturation: 1.1, grayscale: false },
1191
+ simple: { label: 'Simple', brightness: 0.05, contrast: 1.05, saturation: 1.05, grayscale: false },
1192
+ fadecool: { label: 'Fade Cool', brightness: 0.08, contrast: 0.85, saturation: 0.8, grayscale: false },
1193
+ fadewarm: { label: 'Fade Warm', brightness: 0.08, contrast: 0.85, saturation: 0.8, grayscale: false },
1194
+ fade: { label: 'Fade', brightness: 0.1, contrast: 0.8, saturation: 0.75, grayscale: false },
1195
+ vivid: { label: 'Vivid', brightness: 0.05, contrast: 1.25, saturation: 1.4, grayscale: false },
1196
+ cool: { label: 'Cool', brightness: 0.02, contrast: 1.05, saturation: 1.1, grayscale: false },
1197
+ warm: { label: 'Warm', brightness: 0.02, contrast: 1.05, saturation: 1.1, grayscale: false },
1198
+ mono: { label: 'Mono', brightness: 0.0, contrast: 1.1, saturation: 0.0, grayscale: true },
1199
+ noir: { label: 'Noir', brightness: -0.05, contrast: 1.4, saturation: 0.0, grayscale: true },
1200
+ chrome: { label: 'Chrome', brightness: 0.05, contrast: 1.2, saturation: 1.3, grayscale: false },
1201
+ };
1202
+
1203
+
1204
+
1205
+ const imageTransform = useMemo(() => {
1206
+ const transforms = [] as any[];
1207
+ const totalRotation = (imageOptions.rotateDegrees || 0) + straightenAngle;
1208
+ if (totalRotation) {
1209
+ transforms.push({ rotate: `${totalRotation}deg` });
1210
+ }
1211
+ transforms.push({ scaleX: imageOptions.flipX ? -1 : 1 });
1212
+ transforms.push({ scaleY: imageOptions.flipY ? -1 : 1 });
1213
+ return transforms;
1214
+ }, [imageOptions.flipX, imageOptions.flipY, imageOptions.rotateDegrees, straightenAngle]);
1215
+
1216
+ const adjustImage = (patch: Partial<ImageEditOptions>) => {
1217
+ pushToHistory();
1218
+ setImageOptions((prev) => ({ ...prev, ...patch }));
1219
+ };
1220
+
1221
+ const handleSetRatio = (ratio: number | 'custom' | null) => {
1222
+ pushToHistory();
1223
+ setCropRatio(ratio);
1224
+ setCropOffset({ x: 0, y: 0 });
1225
+ setZoomScale(1);
1226
+ setCropResizeScale(1);
1227
+ setCropResizeScaleX(1);
1228
+ setCropResizeScaleY(1);
1229
+ if (ratio === null) {
1230
+ setCrop(null);
1231
+ } else if (ratio === 'custom') {
1232
+ if (!crop) {
1233
+ setCrop({ x: 0, y: 0, width: dimensions.width, height: dimensions.height });
1234
+ }
1235
+ } else {
1236
+ let imgW = dimensions.width;
1237
+ let imgH = dimensions.height;
1238
+ if ((imageOptions.rotateDegrees || 0) % 180 !== 0) {
1239
+ imgW = dimensions.height;
1240
+ imgH = dimensions.width;
1241
+ }
1242
+ if (!imgW || !imgH) return;
1243
+
1244
+ let newW, newH;
1245
+ const imgRatio = imgW / imgH;
1246
+ if (imgRatio > ratio) {
1247
+ newH = imgH;
1248
+ newW = imgH * ratio;
1249
+ } else {
1250
+ newW = imgW;
1251
+ newH = imgW / ratio;
1252
+ }
1253
+ setCrop({
1254
+ x: Math.round((imgW - newW) / 2),
1255
+ y: Math.round((imgH - newH) / 2),
1256
+ width: Math.round(newW),
1257
+ height: Math.round(newH),
1258
+ });
1259
+ }
1260
+ };
1261
+
1262
+ const currentQuickRatioLabel = useMemo(() => {
1263
+ if (cropRatio === 1) return 'Square';
1264
+ if (cropRatio === 9 / 16) return 'Portrait';
1265
+ return 'Portrait';
1266
+ }, [cropRatio]);
1267
+
1268
+ const applyQuickRatio = (mode: 'portrait' | 'square') => {
1269
+ if (mode === 'square') {
1270
+ handleSetRatio(1);
1271
+ } else {
1272
+ handleSetRatio(9 / 16);
1273
+ }
1274
+ setShowQuickRatioMenu(false);
1275
+ };
1276
+
1277
+ useEffect(() => {
1278
+ }, [item.type, panel, cropRatio]);
1279
+
1280
+ useEffect(() => {
1281
+ }, []);
1282
+
1283
+ const applyFilterPreset = (preset: string) => {
1284
+ pushToHistory();
1285
+ setActiveFilter(preset);
1286
+ const filter = (FILTERS as any)[preset] || FILTERS.none;
1287
+ const overlayColor = getPreviewOverlayColor(preset);
1288
+ const overlayOpacity = getPreviewOverlayOpacity(preset);
1289
+ setImageOptions((prev) => ({
1290
+ ...prev,
1291
+ brightness: filter.brightness,
1292
+ contrast: filter.contrast,
1293
+ saturation: filter.saturation,
1294
+ grayscale: filter.grayscale,
1295
+ effect: filter.effect,
1296
+ tintColor: overlayColor !== 'transparent' ? overlayColor : undefined,
1297
+ tintOpacity: overlayColor !== 'transparent' ? overlayOpacity : undefined,
1298
+ }));
1299
+ };
1300
+
1301
+ const getFilterColor = (key: string) => {
1302
+ switch (key) {
1303
+ case 'clarendon': return '#38bdf8';
1304
+ case 'gingham': return '#f1f5f9';
1305
+ case 'moon': return '#64748b';
1306
+ case 'lark': return '#fbbf24';
1307
+ case 'reyes': return '#fed7aa';
1308
+ case 'juno': return '#f43f5e';
1309
+ case 'slumber': return '#a8a29e';
1310
+ case 'crema': return '#fdf4ff';
1311
+ case 'ludwig': return '#ea580c';
1312
+ case 'aden': return '#db2777';
1313
+ case 'perpetua': return '#0d9488';
1314
+ case 'amaro': return '#a5f3fc';
1315
+ case 'mayfair': return '#fda4af';
1316
+ case 'rise': return '#fdba74';
1317
+ case 'valencia': return '#fbbf24';
1318
+ case 'xpro2': return '#4ade80';
1319
+ case 'sierra': return '#a8a29e';
1320
+ case 'willow': return '#94a3b8';
1321
+ case 'lofi': return '#f97316';
1322
+ case 'inkwell': return '#334155';
1323
+ case 'nashville': return '#fecaca';
1324
+ case 'rio': return '#fbbf24';
1325
+ case 'tokyo': return '#22d3ee';
1326
+ case 'cairo': return '#eab308';
1327
+ case 'jaipur': return '#f472b6';
1328
+ case 'newyork': return '#475569';
1329
+ case 'buenosaires': return '#92400e';
1330
+ case 'abudhabi': return '#f59e0b';
1331
+ case 'jakarta': return '#0d9488';
1332
+ case 'melbourne': return '#ecfdf5';
1333
+ case 'oslo': return '#0ea5e9';
1334
+ case 'la': return '#fde047';
1335
+ case 'paris': return '#fbcfe8';
1336
+ case 'emerald': return '#10b981';
1337
+ case 'rosy': return '#fb7185';
1338
+ case 'midnight': return '#1e1b4b';
1339
+ default: return '#334155';
1340
+ }
1341
+ };
1342
+
1343
+ const getPreviewOverlayColor = (key: string) => {
1344
+ switch (key) {
1345
+ case 'clarendon': return '#38bdf8';
1346
+ case 'gingham': return '#e2e8f0';
1347
+ case 'lark': return '#fbbf24';
1348
+ case 'reyes': return '#fff7ed';
1349
+ case 'juno': return '#f43f5e';
1350
+ case 'aden': return '#f472b6';
1351
+ case 'perpetua': return '#2dd4bf';
1352
+ case 'moon': return '#000000';
1353
+ case 'amaro': return '#38bdf8';
1354
+ case 'mayfair': return '#fb7185';
1355
+ case 'rise': return '#f59e0b';
1356
+ case 'valencia': return '#f59e0b';
1357
+ case 'nashville': return '#f472b6';
1358
+ case 'rio': return '#f59e0b';
1359
+ case 'tokyo': return '#06b6d4';
1360
+ case 'cairo': return '#f59e0b';
1361
+ case 'jaipur': return '#ec4899';
1362
+ case 'newyork': return '#64748b';
1363
+ case 'buenosaires': return '#78350f';
1364
+ case 'abudhabi': return '#f59e0b';
1365
+ case 'jakarta': return '#059669';
1366
+ case 'oslo': return '#3b82f6';
1367
+ case 'la': return '#fbbf24';
1368
+ case 'paris': return '#f472b6';
1369
+ case 'boostcool': return '#3b82f6';
1370
+ case 'boostwarm': return '#f59e0b';
1371
+ case 'simplecool': return '#3b82f6';
1372
+ case 'simplewarm': return '#f59e0b';
1373
+ case 'fadecool': return '#3b82f6';
1374
+ case 'fadewarm': return '#f59e0b';
1375
+ case 'colorleak': return '#ef4444';
1376
+ case 'midnight': return '#1e1b4b';
1377
+ case 'emerald': return '#10b981';
1378
+ case 'rosy': return '#f43f5e';
1379
+ case 'vivid': return '#fbbf24';
1380
+ case 'cool': return '#3b82f6';
1381
+ case 'warm': return '#f97316';
1382
+ case 'fade': return '#e2e8f0';
1383
+ case 'mono': return '#6b7280';
1384
+ case 'noir': return '#000000';
1385
+ case 'chrome': return '#06b6d4';
1386
+ default: return 'transparent';
1387
+ }
1388
+ };
1389
+
1390
+ const getPreviewOverlayOpacity = (key: string) => {
1391
+ if (key === 'none') return 0;
1392
+ if (key === 'moon' || key === 'midnight' || key === 'graphite' || key === 'noir') return 0.2;
1393
+ if (key === 'colorleak') return 0.25;
1394
+ if (key === 'halo') return 0.15;
1395
+ if (key === 'mono') return 0.25;
1396
+ if (key === 'fade') return 0.2;
1397
+ return 0.12;
1398
+ };
1399
+
1400
+ const adjustTrim = (deltaStartMs: number, deltaEndMs: number) => {
1401
+ setTrimStart((prev) => Math.max(0, prev + deltaStartMs));
1402
+ setTrimEnd((prev) => Math.max(trimStart + 500, prev + deltaEndMs));
1403
+ };
1404
+
1405
+ const handleDownload = async () => {
1406
+ try {
1407
+ setSaving(true);
1408
+ let exportUri = item.uri;
1409
+ if (item.type === 'image') {
1410
+ exportUri = await editImage(item.uri, activeOptions);
1411
+ if (selectedMusic) {
1412
+ exportUri = await trimVideo(exportUri, {
1413
+ isImage: true,
1414
+ musicUri: selectedMusic.url,
1415
+ rotateDegrees: 0,
1416
+ flipX: false,
1417
+ flipY: false,
1418
+ brightness: 0,
1419
+ contrast: 1,
1420
+ saturation: 1,
1421
+ grayscale: false,
1422
+ });
1423
+ }
1424
+ } else {
1425
+ exportUri = await trimVideo(item.uri, {
1426
+ startMs: trimStart,
1427
+ endMs: trimEnd,
1428
+ mute: isMuted,
1429
+ musicUri: selectedMusic?.url || undefined,
1430
+ ...activeOptions,
1431
+ });
1432
+ }
1433
+ await saveToGallery(exportUri, (selectedMusic || item.type === 'video') ? 'video' : 'image');
1434
+ Alert.alert('Success', 'Media saved to your gallery!');
1435
+ } catch (err: any) {
1436
+ Alert.alert('Download failed', err?.message ?? 'Could not save to gallery.');
1437
+ } finally {
1438
+ setSaving(false);
1439
+ }
1440
+ };
1441
+
1442
+ const handleSaveAll = async () => {
1443
+ try {
1444
+ setSaving(true);
1445
+ saveEditsForIndex(activeIndex);
1446
+
1447
+ const updatedItems = [...items];
1448
+
1449
+ for (let i = 0; i < items.length; i++) {
1450
+ const targetItem = items[i];
1451
+
1452
+ let edits = editsHistoryRef.current[targetItem.id];
1453
+ if (i === activeIndex) {
1454
+ edits = {
1455
+ activeFilter,
1456
+ imageOptions,
1457
+ trimStart,
1458
+ trimEnd,
1459
+ overlays,
1460
+ cropRatio,
1461
+ cropOffset,
1462
+ zoomScale,
1463
+ straightenAngle,
1464
+ isMuted,
1465
+ };
1466
+ }
1467
+
1468
+ if (edits) {
1469
+ const opts = i === activeIndex ? activeOptions : buildOptionsForItem(targetItem, edits);
1470
+
1471
+ if (targetItem.type === 'image') {
1472
+ let outUri = await editImage(targetItem.uri, opts);
1473
+ if (selectedMusic) {
1474
+ outUri = await trimVideo(outUri, {
1475
+ isImage: true,
1476
+ musicUri: selectedMusic.url,
1477
+ rotateDegrees: 0,
1478
+ flipX: false,
1479
+ flipY: false,
1480
+ brightness: 0,
1481
+ contrast: 1,
1482
+ saturation: 1,
1483
+ grayscale: false,
1484
+ });
1485
+ }
1486
+ updatedItems[i] = {
1487
+ ...targetItem,
1488
+ uri: outUri,
1489
+ thumbnailUri: outUri,
1490
+ };
1491
+ } else {
1492
+ const outUri = await trimVideo(targetItem.uri, {
1493
+ startMs: edits.trimStart,
1494
+ endMs: edits.trimEnd,
1495
+ mute: edits.isMuted,
1496
+ musicUri: selectedMusic?.url || undefined,
1497
+ ...opts,
1498
+ });
1499
+
1500
+ let newThumb = undefined;
1501
+ try {
1502
+ newThumb = await captureFrame(outUri, { timeMs: 0 });
1503
+ } catch (e) {
1504
+ console.warn('Could not generate filtered thumb', e);
1505
+ }
1506
+
1507
+ const newDuration = edits.trimEnd - edits.trimStart;
1508
+ updatedItems[i] = {
1509
+ ...targetItem,
1510
+ uri: outUri,
1511
+ thumbnailUri: newThumb ? newThumb : targetItem.thumbnailUri,
1512
+ durationMs: newDuration,
1513
+ };
1514
+ }
1515
+ } else {
1516
+ if (selectedMusic) {
1517
+ let outUri = targetItem.uri;
1518
+ if (targetItem.type === 'image') {
1519
+ outUri = await trimVideo(targetItem.uri, {
1520
+ isImage: true,
1521
+ musicUri: selectedMusic.url,
1522
+ rotateDegrees: 0,
1523
+ flipX: false,
1524
+ flipY: false,
1525
+ brightness: 0,
1526
+ contrast: 1,
1527
+ saturation: 1,
1528
+ grayscale: false,
1529
+ });
1530
+ } else {
1531
+ outUri = await trimVideo(targetItem.uri, {
1532
+ startMs: 0,
1533
+ endMs: targetItem.durationMs || 10000,
1534
+ mute: isMuted,
1535
+ musicUri: selectedMusic.url,
1536
+ rotateDegrees: 0,
1537
+ flipX: false,
1538
+ flipY: false,
1539
+ brightness: 0,
1540
+ contrast: 1,
1541
+ saturation: 1,
1542
+ grayscale: false,
1543
+ });
1544
+ }
1545
+ updatedItems[i] = {
1546
+ ...targetItem,
1547
+ uri: outUri,
1548
+ thumbnailUri: targetItem.type === 'image' ? outUri : targetItem.thumbnailUri,
1549
+ };
1550
+ }
1551
+ }
1552
+ }
1553
+
1554
+ onSaved(updatedItems);
1555
+ } catch (err: any) {
1556
+ console.error('EXPORT / SAVE EDITS FAILED:', err?.message ?? err);
1557
+ } finally {
1558
+ setSaving(false);
1559
+ }
1560
+ };
1561
+
1562
+ const renderCard = ({ item: cardItem, index }: { item: MediaItem; index: number }) => {
1563
+ const isActive = index === activeIndex;
1564
+
1565
+ // Read the edits for this item
1566
+ const edits = isActive
1567
+ ? {
1568
+ activeFilter,
1569
+ imageOptions,
1570
+ overlays,
1571
+ cropRatio,
1572
+ cropOffset,
1573
+ zoomScale,
1574
+ straightenAngle,
1575
+ }
1576
+ : editsHistoryRef.current[cardItem.id] || {
1577
+ activeFilter: 'none',
1578
+ imageOptions: { frame: '' },
1579
+ overlays: [],
1580
+ cropRatio: null,
1581
+ cropOffset: { x: 0, y: 0 },
1582
+ zoomScale: 1,
1583
+ straightenAngle: 0,
1584
+ };
1585
+
1586
+ const frameConfig = (FRAME_CONFIGS as any)[edits.imageOptions.frame || ''] || { scale: 1, offsetY: 0 };
1587
+ const currentScale = edits.imageOptions.frame ? frameConfig.scale : 1;
1588
+ const cardDim = dimensionsMap[cardItem.id] || { width: 1080, height: 1920 };
1589
+ const currentYOffset = edits.imageOptions.frame ? (frameConfig.offsetY || 0) * (cardDim.height || 1000) : 0;
1590
+
1591
+ const cardTransform = [];
1592
+ if (edits.imageOptions.rotateDegrees) {
1593
+ cardTransform.push({ rotate: `${edits.imageOptions.rotateDegrees}deg` });
1594
+ }
1595
+ if (edits.imageOptions.flipX) {
1596
+ cardTransform.push({ scaleX: -1 });
1597
+ }
1598
+ if (edits.imageOptions.flipY) {
1599
+ cardTransform.push({ scaleY: -1 });
1600
+ }
1601
+
1602
+ const cardAspect = typeof edits.cropRatio === 'number' ? edits.cropRatio : (cardDim.width / (cardDim.height || 1));
1603
+
1604
+ return (
1605
+ <View style={[styles.cardContainer, { width: CARD_WIDTH, marginHorizontal: CARD_MARGIN }]}>
1606
+ <View
1607
+ style={[
1608
+ styles.cardPreviewBox,
1609
+ {
1610
+ aspectRatio: cardAspect,
1611
+ backgroundColor: '#111',
1612
+ borderRadius: 16,
1613
+ overflow: 'hidden',
1614
+ width: '100%',
1615
+ maxWidth: CARD_WIDTH,
1616
+ alignSelf: 'center',
1617
+ }
1618
+ ]}
1619
+ {...(isActive && panel === 'transform' && cardItem.type === 'image' ? cropPan.panHandlers : {})}
1620
+ >
1621
+ {cardItem.type === 'image' ? (
1622
+ <Image
1623
+ source={{ uri: cardItem.uri }}
1624
+ style={[
1625
+ styles.preview,
1626
+ {
1627
+ transform: [
1628
+ { scale: currentScale },
1629
+ { translateX: edits.cropOffset.x },
1630
+ { translateY: edits.cropOffset.y + currentYOffset },
1631
+ { scale: edits.zoomScale },
1632
+ ...cardTransform
1633
+ ]
1634
+ }
1635
+ ]}
1636
+ resizeMode={edits.cropRatio ? "cover" : "contain"}
1637
+ />
1638
+ ) : (
1639
+ <Pressable onPress={() => isActive && setVideoPaused(v => !v)} style={[styles.videoPreview, { transform: [{ scale: currentScale }, { translateX: edits.cropOffset.x }, { translateY: edits.cropOffset.y + currentYOffset }, { scale: edits.zoomScale }, ...cardTransform] }]}>
1640
+ <VideoPreview
1641
+ uri={cardItem.uri}
1642
+ paused={!isActive || videoPaused}
1643
+ muted={isActive ? isMuted : true}
1644
+ style={styles.previewContainerResized}
1645
+ resizeMode={edits.cropRatio ? "cover" : "contain"}
1646
+ />
1647
+ {isActive && videoPaused && (
1648
+ <View style={styles.previewOverlay}>
1649
+ <View style={styles.playPauseCircle}>
1650
+ <Ionicons name="play" size={22} color="#fff" />
1651
+ </View>
1652
+ </View>
1653
+ )}
1654
+ </Pressable>
1655
+ )}
1656
+
1657
+ {/* Frame Overlay */}
1658
+ {edits.imageOptions.frame && FRAME_IMAGES[edits.imageOptions.frame] && (
1659
+ <View
1660
+ style={[StyleSheet.absoluteFill, { zIndex: 10, justifyContent: 'center', alignItems: 'center' }]}
1661
+ pointerEvents="none"
1662
+ >
1663
+ <Image
1664
+ source={FRAME_IMAGES[edits.imageOptions.frame]}
1665
+ style={{ width: '100%', height: '100%' }}
1666
+ resizeMode="stretch"
1667
+ />
1668
+ </View>
1669
+ )}
1670
+
1671
+ {/* Color Filter Overlay */}
1672
+ <View
1673
+ pointerEvents="none"
1674
+ style={[
1675
+ StyleSheet.absoluteFill,
1676
+ {
1677
+ backgroundColor: getPreviewOverlayColor(edits.activeFilter),
1678
+ opacity: getPreviewOverlayOpacity(edits.activeFilter),
1679
+ zIndex: 8,
1680
+ },
1681
+ ]}
1682
+ />
1683
+
1684
+ {/* Brightness Overlay */}
1685
+ <View
1686
+ pointerEvents="none"
1687
+ style={[
1688
+ StyleSheet.absoluteFill,
1689
+ {
1690
+ backgroundColor: (edits.imageOptions.brightness ?? 0) > 0 ? '#fff' : '#000',
1691
+ opacity: Math.min(0.6, Math.abs(edits.imageOptions.brightness ?? 0) * 0.5),
1692
+ zIndex: 9,
1693
+ },
1694
+ ]}
1695
+ />
1696
+
1697
+ {/* Text Overlays */}
1698
+ {isActive && overlays.map((overlay) => {
1699
+ const responder = getTextResponder(overlay.id);
1700
+ const isSelected = editingTextId === overlay.id;
1701
+ return (
1702
+ <View
1703
+ key={overlay.id}
1704
+ style={[
1705
+ styles.textOverlayContainer,
1706
+ { left: overlay.x, top: overlay.y, zIndex: 20 },
1707
+ isSelected && styles.selectedTextContainer
1708
+ ]}
1709
+ {...responder.panHandlers}
1710
+ >
1711
+ <Pressable
1712
+ onLongPress={() => removeTextOverlay(overlay.id)}
1713
+ onPress={() => {
1714
+ pushToHistory();
1715
+ // Backup original state (text, color, position, size, etc.)
1716
+ const found = overlays.find(o => o.id === overlay.id);
1717
+ if (found) {
1718
+ originalOverlayBackup.current = { ...found };
1719
+ }
1720
+ isNewOverlay.current = false;
1721
+ setEditingTextId(overlay.id);
1722
+ setNewText(overlay.text);
1723
+ }}
1724
+ >
1725
+ <Text style={[styles.textOverlay, { color: overlay.color, fontSize: overlay.fontSize }]}>
1726
+ {overlay.text}
1727
+ </Text>
1728
+ </Pressable>
1729
+ </View>
1730
+ );
1731
+ })}
1732
+
1733
+ {!isActive && edits.overlays && edits.overlays.map((overlay: any) => (
1734
+ <View
1735
+ key={overlay.id}
1736
+ style={[
1737
+ styles.textOverlayContainer,
1738
+ { left: overlay.x, top: overlay.y, zIndex: 20 }
1739
+ ]}
1740
+ pointerEvents="none"
1741
+ >
1742
+ <Text style={[styles.textOverlay, { color: overlay.color, fontSize: overlay.fontSize }]}>
1743
+ {overlay.text}
1744
+ </Text>
1745
+ </View>
1746
+ ))}
1747
+
1748
+ {/* Sticker Overlays */}
1749
+ {isActive && stickers.map(s => (
1750
+ <View
1751
+ key={s.id}
1752
+ style={{ position: 'absolute', left: s.x, top: s.y, zIndex: 25 }}
1753
+ pointerEvents="box-none"
1754
+ >
1755
+ <Pressable onLongPress={() => removeSticker(s.id)}>
1756
+ <Text style={{ fontSize: s.size }}>{s.emoji}</Text>
1757
+ </Pressable>
1758
+ </View>
1759
+ ))}
1760
+
1761
+ {/* Caption Overlays */}
1762
+ {isActive && captions.map(c => {
1763
+ const cs = CAPTION_STYLES.find(s => s.id === c.style) || CAPTION_STYLES[0];
1764
+ return (
1765
+ <View
1766
+ key={c.id}
1767
+ style={{ position: 'absolute', left: c.x, top: c.y, zIndex: 26, maxWidth: 220 }}
1768
+ pointerEvents="box-none"
1769
+ >
1770
+ <Pressable onLongPress={() => removeCaption(c.id)}>
1771
+ <View style={{ backgroundColor: cs.bg, paddingHorizontal: 10, paddingVertical: 5, borderRadius: 6 }}>
1772
+ <Text style={{ color: cs.color, fontWeight: cs.fontWeight, fontSize: 14 }}>{c.text}</Text>
1773
+ </View>
1774
+ </Pressable>
1775
+ </View>
1776
+ );
1777
+ })}
1778
+
1779
+ {/* Floating Crop Button */}
1780
+ {isActive && cardItem.type === 'image' && (
1781
+ <Pressable style={styles.cropIconBtn} onPress={() => onOpenCrop(cardItem)}>
1782
+ <Ionicons name="crop-outline" size={20} color="#fff" />
1783
+ </Pressable>
1784
+ )}
1785
+ </View>
1786
+ </View>
1787
+ );
1788
+ };
1789
+
1790
+ const videoAspect = typeof cropRatio === 'number' ? cropRatio : (dimensions.width / (dimensions.height || 1));
1791
+
1792
+ const carouselHeight = showMusicModal ? 280 : 380;
1793
+
1794
+ return (
1795
+ <View style={styles.container}>
1796
+ {selectedMusic && (
1797
+ <Video
1798
+ source={{ uri: selectedMusic.url }}
1799
+ paused={musicPaused || (item.type === 'video' ? videoPaused : false)}
1800
+ repeat
1801
+ muted={false}
1802
+ style={{ width: 0, height: 0, position: 'absolute' }}
1803
+ />
1804
+ )}
1805
+
1806
+ {items.length === 1 && item.type === 'video' ? (
1807
+ isEditingVideo ? (
1808
+ /* Render Edit Mode UI (Screenshot 2) */
1809
+ <View style={styles.editModeContainer}>
1810
+ {/* Top Header */}
1811
+ <View style={styles.editModeHeader}>
1812
+ <Pressable onPress={() => setIsEditingVideo(false)} style={styles.editModeBackBtn}>
1813
+ <Ionicons name="chevron-down" size={22} color="#fff" />
1814
+ </Pressable>
1815
+ <Pressable onPress={handleSaveAll} style={styles.editModeNextBtn} disabled={saving}>
1816
+ {saving ? (
1817
+ <ActivityIndicator size="small" color="#fff" />
1818
+ ) : (
1819
+ <Ionicons name="arrow-forward" size={20} color="#fff" />
1820
+ )}
1821
+ </Pressable>
1822
+ </View>
1823
+
1824
+ {/* Centered Small Video Player */}
1825
+ <View
1826
+ style={styles.editModePlayerContainer}
1827
+ {...(panel === 'transform' ? cropPan.panHandlers : {})}
1828
+ >
1829
+ <View
1830
+ style={{
1831
+ width: '100%',
1832
+ aspectRatio: videoAspect || 1,
1833
+ maxHeight: '100%',
1834
+ justifyContent: 'center',
1835
+ alignItems: 'center',
1836
+ overflow: 'hidden',
1837
+ backgroundColor: '#000',
1838
+ }}
1839
+ >
1840
+ <VideoPreview
1841
+ uri={item.uri}
1842
+ paused={videoPaused}
1843
+ muted={isMuted}
1844
+ style={[
1845
+ styles.editModeVideo,
1846
+ {
1847
+ transform: [
1848
+ { scale: zoomScale },
1849
+ { translateX: cropOffset.x },
1850
+ { translateY: cropOffset.y }
1851
+ ]
1852
+ }
1853
+ ]}
1854
+ resizeMode={cropRatio ? "cover" : "contain"}
1855
+ trimStartMs={trimStart}
1856
+ trimEndMs={trimEnd}
1857
+ seekToMs={seekToMs}
1858
+ onChange={(e) => {
1859
+ const time = e.nativeEvent.currentTimeMs;
1860
+ setCurrentTimeMs(time);
1861
+ if (!isUserScrolling.current && !isDraggingHandle.current) {
1862
+ const x = (time / duration) * TIMELINE_WIDTH;
1863
+ timelineScrollRef.current?.scrollTo({ x, animated: false });
1864
+ }
1865
+ }}
1866
+ />
1867
+
1868
+ {/* Frame/Overlay Overlay */}
1869
+ {imageOptions.frame && FRAME_IMAGES[imageOptions.frame] && (
1870
+ <View
1871
+ style={[StyleSheet.absoluteFill, { zIndex: 10, justifyContent: 'center', alignItems: 'center' }]}
1872
+ pointerEvents="none"
1873
+ >
1874
+ <Image
1875
+ source={FRAME_IMAGES[imageOptions.frame]}
1876
+ style={{ width: '100%', height: '100%' }}
1877
+ resizeMode="stretch"
1878
+ />
1879
+ </View>
1880
+ )}
1881
+
1882
+ {/* Color Filter Overlay */}
1883
+ <View
1884
+ pointerEvents="none"
1885
+ style={[
1886
+ StyleSheet.absoluteFill,
1887
+ {
1888
+ backgroundColor: getPreviewOverlayColor(activeFilter),
1889
+ opacity: getPreviewOverlayOpacity(activeFilter),
1890
+ zIndex: 8,
1891
+ },
1892
+ ]}
1893
+ />
1894
+
1895
+ {/* Brightness Overlay */}
1896
+ <View
1897
+ pointerEvents="none"
1898
+ style={[
1899
+ StyleSheet.absoluteFill,
1900
+ {
1901
+ backgroundColor: (imageOptions.brightness ?? 0) > 0 ? '#fff' : '#000',
1902
+ opacity: Math.min(0.6, Math.abs(imageOptions.brightness ?? 0) * 0.5),
1903
+ zIndex: 9,
1904
+ },
1905
+ ]}
1906
+ />
1907
+
1908
+ {/* Low Contrast Overlay (emulated gray layer) */}
1909
+ {(imageOptions.contrast ?? 1) < 1 && (
1910
+ <View
1911
+ pointerEvents="none"
1912
+ style={[
1913
+ StyleSheet.absoluteFill,
1914
+ {
1915
+ backgroundColor: '#808080',
1916
+ opacity: (1 - (imageOptions.contrast ?? 1)) * 0.4,
1917
+ zIndex: 9,
1918
+ },
1919
+ ]}
1920
+ />
1921
+ )}
1922
+
1923
+ {/* High Contrast Overlay (emulated shadow enhancement) */}
1924
+ {(imageOptions.contrast ?? 1) > 1 && (
1925
+ <View
1926
+ pointerEvents="none"
1927
+ style={[
1928
+ StyleSheet.absoluteFill,
1929
+ {
1930
+ backgroundColor: '#000',
1931
+ opacity: ((imageOptions.contrast ?? 1) - 1) * 0.15,
1932
+ zIndex: 9,
1933
+ },
1934
+ ]}
1935
+ />
1936
+ )}
1937
+
1938
+ {/* Low Saturation / Grayscale Overlay (emulated gray desaturation) */}
1939
+ {(imageOptions.saturation ?? 1) < 1 && (
1940
+ <View
1941
+ pointerEvents="none"
1942
+ style={[
1943
+ StyleSheet.absoluteFill,
1944
+ {
1945
+ backgroundColor: '#808080',
1946
+ opacity: (1 - (imageOptions.saturation ?? 1)) * 0.5,
1947
+ zIndex: 9,
1948
+ },
1949
+ ]}
1950
+ />
1951
+ )}
1952
+
1953
+ {/* Text Overlays */}
1954
+ {overlays.map((overlay) => {
1955
+ const responder = getTextResponder(overlay.id);
1956
+ const isSelected = editingTextId === overlay.id;
1957
+ return (
1958
+ <View
1959
+ key={overlay.id}
1960
+ style={[
1961
+ styles.textOverlayContainer,
1962
+ { left: overlay.x, top: overlay.y, zIndex: 20 },
1963
+ isSelected && styles.selectedTextContainer
1964
+ ]}
1965
+ {...responder.panHandlers}
1966
+ >
1967
+ <Pressable
1968
+ onLongPress={() => removeTextOverlay(overlay.id)}
1969
+ onPress={() => {
1970
+ pushToHistory();
1971
+ const found = overlays.find(o => o.id === overlay.id);
1972
+ if (found) {
1973
+ originalOverlayBackup.current = { ...found };
1974
+ }
1975
+ isNewOverlay.current = false;
1976
+ setEditingTextId(overlay.id);
1977
+ setNewText(overlay.text);
1978
+ }}
1979
+ >
1980
+ <Text style={[styles.textOverlay, { color: overlay.color, fontSize: overlay.fontSize }]}>
1981
+ {overlay.text}
1982
+ </Text>
1983
+ </Pressable>
1984
+ </View>
1985
+ );
1986
+ })}
1987
+ </View>
1988
+ </View>
1989
+
1990
+ {/* Video Player Controls */}
1991
+ <View style={styles.playerControlsRow}>
1992
+ <Pressable onPress={() => setVideoPaused(!videoPaused)} style={styles.editPlayPauseBtn}>
1993
+ <Ionicons name={videoPaused ? 'play' : 'pause'} size={20} color="#fff" />
1994
+ </Pressable>
1995
+ <Text style={styles.durationText}>
1996
+ {formatTime(currentTimeMs)} / {formatTime(duration)}
1997
+ </Text>
1998
+
1999
+ <View style={styles.historyButtons}>
2000
+ <Pressable
2001
+ style={[styles.historyBtn, undoStack.length === 0 && { opacity: 0.5 }]}
2002
+ onPress={handleUndo}
2003
+ disabled={undoStack.length === 0}
2004
+ >
2005
+ <Ionicons name="arrow-undo" size={18} color="#fff" />
2006
+ </Pressable>
2007
+ <Pressable
2008
+ style={[styles.historyBtn, redoStack.length === 0 && { opacity: 0.5 }]}
2009
+ onPress={handleRedo}
2010
+ disabled={redoStack.length === 0}
2011
+ >
2012
+ <Ionicons name="arrow-redo" size={18} color="#fff" />
2013
+ </Pressable>
2014
+ </View>
2015
+ </View>
2016
+
2017
+ {/* Timeline View */}
2018
+ <View style={styles.timelineSection}>
2019
+ {/* Scrollable Tracks & Ruler */}
2020
+ <ScrollView
2021
+ horizontal
2022
+ ref={timelineScrollRef}
2023
+ showsHorizontalScrollIndicator={false}
2024
+ scrollEnabled={scrollEnabled}
2025
+ onScroll={handleTimelineScroll}
2026
+ scrollEventThrottle={16}
2027
+ onScrollBeginDrag={() => {
2028
+ isUserScrolling.current = true;
2029
+ // Pause video when user starts scrubbing the timeline
2030
+ setVideoPaused(true);
2031
+ }}
2032
+ onScrollEndDrag={() => {
2033
+ if (!isMomentumScrolling.current) {
2034
+ isUserScrolling.current = false;
2035
+ setSeekToMs(-1);
2036
+ }
2037
+ }}
2038
+ onMomentumScrollBegin={() => {
2039
+ isMomentumScrolling.current = true;
2040
+ }}
2041
+ onMomentumScrollEnd={() => {
2042
+ isMomentumScrolling.current = false;
2043
+ isUserScrolling.current = false;
2044
+ setSeekToMs(-1);
2045
+ }}
2046
+ contentContainerStyle={{
2047
+ paddingHorizontal: SCREEN_WIDTH / 2,
2048
+ }}
2049
+ >
2050
+ <View style={{ width: TIMELINE_WIDTH }}>
2051
+ {/* Ruler */}
2052
+ <View style={styles.timelineRuler}>
2053
+ <View style={styles.timelineRulerDot} />
2054
+ <Text style={styles.timelineRulerText}>5s</Text>
2055
+ <View style={styles.timelineRulerDot} />
2056
+ <Text style={styles.timelineRulerText}>10s</Text>
2057
+ <View style={styles.timelineRulerDot} />
2058
+ <Text style={styles.timelineRulerText}>15s</Text>
2059
+ <View style={styles.timelineRulerDot} />
2060
+ </View>
2061
+
2062
+ <View style={styles.timelineTracksContainer}>
2063
+ {/* Filmstrip video track */}
2064
+ <View style={[styles.filmstripTrack, { overflow: 'visible' }]}>
2065
+ <Pressable
2066
+ style={styles.filmstrip}
2067
+ onPress={(e) => {
2068
+ const x = e.nativeEvent.locationX;
2069
+ // Pause video and seek to the tapped position
2070
+ setVideoPaused(true);
2071
+ timelineScrollRef.current?.scrollTo({ x: x - SCREEN_WIDTH / 2, animated: true });
2072
+ const time = (x / TIMELINE_WIDTH) * duration;
2073
+ const clampedTime = Math.max(trimStart, Math.min(trimEnd, time));
2074
+ setCurrentTimeMs(clampedTime);
2075
+ throttledSeek(clampedTime);
2076
+ }}
2077
+ >
2078
+ {thumbnails.map((uri, idx) => (
2079
+ <Image key={idx} source={{ uri }} style={styles.filmstripImage} />
2080
+ ))}
2081
+ <View style={styles.timelineOverlay} />
2082
+ <View
2083
+ style={[
2084
+ styles.selectionRange,
2085
+ { left: (trimStart / duration) * TIMELINE_WIDTH, width: ((trimEnd - trimStart) / duration) * TIMELINE_WIDTH }
2086
+ ]}
2087
+ />
2088
+ </Pressable>
2089
+ <View
2090
+ style={[
2091
+ styles.customHandle,
2092
+ styles.customHandleLeft,
2093
+ { left: (trimStart / duration) * TIMELINE_WIDTH - 16 }
2094
+ ]}
2095
+ {...startPan.panHandlers}
2096
+ >
2097
+ <View style={styles.handleBarLine} />
2098
+ </View>
2099
+ <View
2100
+ style={[
2101
+ styles.customHandle,
2102
+ styles.customHandleRight,
2103
+ { left: (trimEnd / duration) * TIMELINE_WIDTH }
2104
+ ]}
2105
+ {...endPan.panHandlers}
2106
+ >
2107
+ <View style={styles.handleBarLine} />
2108
+ </View>
2109
+ </View>
2110
+
2111
+ {/* Sub-track 1: Add Audio */}
2112
+ <Pressable style={styles.subTrackRow} onPress={() => setShowMusicModal(true)}>
2113
+ <Text style={styles.subTrackIcon}>+</Text>
2114
+ <Text style={styles.subTrackText}>
2115
+ {selectedMusic ? `Audio: ${selectedMusic.title}` : 'Add audio'}
2116
+ </Text>
2117
+ </Pressable>
2118
+
2119
+ {/* Sub-track 2: Add Text */}
2120
+ <Pressable style={styles.subTrackRow} onPress={addTextOverlay}>
2121
+ <Text style={styles.subTrackIcon}>+</Text>
2122
+ <Text style={styles.subTrackText}>
2123
+ {overlays.length > 0 ? `Text overlays (${overlays.length})` : 'Add text'}
2124
+ </Text>
2125
+ </Pressable>
2126
+ </View>
2127
+ </View>
2128
+ </ScrollView>
2129
+
2130
+ {/* Fixed Playhead indicator in the center */}
2131
+ <View style={styles.timelineCenterLine} pointerEvents="none" />
2132
+
2133
+ <Text style={styles.timelineHintText}>
2134
+ Tap on a track to seek.
2135
+ </Text>
2136
+ </View>
2137
+
2138
+ {/* Inline Panel Section — shows when filter/overlay/edit/transform is selected */}
2139
+ {(panel === 'filter' || panel === 'frame' || panel === 'edit' || panel === 'transform') && (
2140
+ <View style={{ backgroundColor: '#111', maxHeight: 130, borderTopWidth: 1, borderTopColor: '#222' }}>
2141
+ {panel === 'filter' && (
2142
+ <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ paddingHorizontal: 12, paddingVertical: 10, gap: 10 }}>
2143
+ {['none', 'vivid', 'cool', 'warm', 'fade', 'mono', 'noir', 'chrome'].map((f) => (
2144
+ <Pressable key={f} onPress={() => applyFilterPreset(f)} style={{ alignItems: 'center', gap: 4 }}>
2145
+ <View style={{
2146
+ width: 56, height: 56, borderRadius: 10, backgroundColor: '#222',
2147
+ borderWidth: activeFilter === f ? 2 : 0, borderColor: '#38bdf8',
2148
+ justifyContent: 'center', alignItems: 'center'
2149
+ }}>
2150
+ <Text style={{ fontSize: 20 }}>
2151
+ {f === 'none' ? '○' : f === 'vivid' ? '🌈' : f === 'cool' ? '❄️' : f === 'warm' ? '🔥' : f === 'fade' ? '🌫️' : f === 'mono' ? '⬛' : f === 'noir' ? '🖤' : '📷'}
2152
+ </Text>
2153
+ </View>
2154
+ <Text style={{ color: activeFilter === f ? '#38bdf8' : '#aaa', fontSize: 10, textTransform: 'capitalize' }}>{f}</Text>
2155
+ </Pressable>
2156
+ ))}
2157
+ </ScrollView>
2158
+ )}
2159
+ {panel === 'edit' && (
2160
+ <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ paddingHorizontal: 12, paddingVertical: 8 }}>
2161
+ <AdjustItem label="Brightness" value={imageOptions.brightness ?? 0} onAdjust={(v) => adjustImage({ brightness: v })} min={-1} max={1} />
2162
+ <AdjustItem label="Contrast" value={imageOptions.contrast ?? 1} onAdjust={(v) => adjustImage({ contrast: v })} min={0} max={2} />
2163
+ <AdjustItem label="Saturation" value={imageOptions.saturation ?? 1} onAdjust={(v) => adjustImage({ saturation: v })} min={0} max={2} />
2164
+ </ScrollView>
2165
+ )}
2166
+ {panel === 'transform' && (
2167
+ <View style={styles.panelInner}>
2168
+ <Text style={styles.panelTitle}>Aspect Ratio</Text>
2169
+ <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ paddingHorizontal: 12, paddingVertical: 8, gap: 16 }}>
2170
+ {[
2171
+ { label: 'Original', ratio: null },
2172
+ { label: 'Square 1:1', ratio: 1 },
2173
+ { label: 'Portrait 4:5', ratio: 4 / 5 },
2174
+ { label: 'Stories 9:16', ratio: 9 / 16 },
2175
+ { label: 'Landscape 16:9', ratio: 16 / 9 },
2176
+ ].map((r) => {
2177
+ const isSelected = cropRatio === r.ratio;
2178
+ return (
2179
+ <Pressable
2180
+ key={r.label}
2181
+ onPress={() => handleSetRatio(r.ratio)}
2182
+ style={{
2183
+ alignItems: 'center',
2184
+ justifyContent: 'center',
2185
+ paddingHorizontal: 12,
2186
+ paddingVertical: 6,
2187
+ borderRadius: 8,
2188
+ backgroundColor: isSelected ? '#38bdf8' : '#222',
2189
+ borderWidth: 1,
2190
+ borderColor: isSelected ? '#38bdf8' : '#333',
2191
+ }}
2192
+ >
2193
+ <Text style={{ color: isSelected ? '#000' : '#fff', fontSize: 12, fontWeight: '600' }}>
2194
+ {r.label}
2195
+ </Text>
2196
+ </Pressable>
2197
+ );
2198
+ })}
2199
+ </ScrollView>
2200
+ <Text style={{ color: '#888', fontSize: 11, textAlign: 'center', marginBottom: 4 }}>
2201
+ Pinch to zoom and drag to position the video
2202
+ </Text>
2203
+ </View>
2204
+ )}
2205
+ {panel === 'frame' && (
2206
+ <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ paddingHorizontal: 12, paddingVertical: 10, gap: 12 }}>
2207
+ <Pressable
2208
+ style={{ alignItems: 'center', gap: 4 }}
2209
+ onPress={() => adjustImage({ frame: '' })}
2210
+ >
2211
+ <View style={{
2212
+ width: 56, height: 56, borderRadius: 10, backgroundColor: '#333',
2213
+ borderWidth: !imageOptions.frame ? 2 : 0, borderColor: '#38bdf8',
2214
+ justifyContent: 'center', alignItems: 'center'
2215
+ }}>
2216
+ <Ionicons name="close" size={20} color="#fff" />
2217
+ </View>
2218
+ <Text style={{ color: !imageOptions.frame ? '#38bdf8' : '#aaa', fontSize: 10 }}>None</Text>
2219
+ </Pressable>
2220
+ {FRAME_LIST.map((f) => {
2221
+ const isActive = imageOptions.frame === f.key;
2222
+ return (
2223
+ <Pressable key={f.key} onPress={() => adjustImage({ frame: f.key })} style={{ alignItems: 'center', gap: 4 }}>
2224
+ <View style={{
2225
+ width: 56, height: 56, borderRadius: 10, backgroundColor: '#222',
2226
+ borderWidth: isActive ? 2 : 0, borderColor: '#38bdf8',
2227
+ overflow: 'hidden', justifyContent: 'center', alignItems: 'center'
2228
+ }}>
2229
+ {FRAME_IMAGES[f.key] ? (
2230
+ <Image source={FRAME_IMAGES[f.key]} style={{ width: '100%', height: '100%' }} resizeMode="stretch" />
2231
+ ) : (
2232
+ <Text style={{ fontSize: 22 }}>🖼️</Text>
2233
+ )}
2234
+ </View>
2235
+ <Text style={{ color: isActive ? '#38bdf8' : '#aaa', fontSize: 10 }}>{f.label}</Text>
2236
+ </Pressable>
2237
+ );
2238
+ })}
2239
+ </ScrollView>
2240
+ )}
2241
+ </View>
2242
+ )}
2243
+
2244
+ {/* Bottom Toolbar — 6 working tools */}
2245
+ <View style={styles.bottomToolBarContainer}>
2246
+ <ScrollView
2247
+ horizontal
2248
+ showsHorizontalScrollIndicator={false}
2249
+ style={{ flexGrow: 0 }}
2250
+ contentContainerStyle={[styles.toolButtonsRow, { flexGrow: 1, backgroundColor: '#000', paddingVertical: 12 }]}
2251
+ >
2252
+ <Pressable style={styles.toolButton} onPress={addTextOverlay}>
2253
+ <View style={styles.toolIconContainer}>
2254
+ <Ionicons name="text" size={22} color="#fff" />
2255
+ </View>
2256
+ <Text style={styles.toolLabel}>Text</Text>
2257
+ </Pressable>
2258
+ <Pressable style={[styles.toolButton, showMusicModal && styles.toolButtonActive]} onPress={() => setShowMusicModal(true)}>
2259
+ <View style={styles.toolIconContainer}>
2260
+ <Ionicons name="musical-notes" size={22} color="#fff" />
2261
+ </View>
2262
+ <Text style={styles.toolLabel}>Audio</Text>
2263
+ </Pressable>
2264
+ <Pressable style={[styles.toolButton, panel === 'transform' && styles.toolButtonActive]} onPress={() => setPanel(panel === 'transform' ? 'trim' : 'transform')}>
2265
+ <View style={styles.toolIconContainer}>
2266
+ <Ionicons name="crop" size={22} color="#fff" />
2267
+ </View>
2268
+ <Text style={styles.toolLabel}>Crop</Text>
2269
+ </Pressable>
2270
+ <Pressable style={[styles.toolButton, panel === 'filter' && styles.toolButtonActive]} onPress={() => setPanel(panel === 'filter' ? 'trim' : 'filter')}>
2271
+ <View style={styles.toolIconContainer}>
2272
+ <Ionicons name="color-palette" size={22} color="#fff" />
2273
+ </View>
2274
+ <Text style={styles.toolLabel}>Filter</Text>
2275
+ </Pressable>
2276
+ <Pressable style={[styles.toolButton, panel === 'frame' && styles.toolButtonActive]} onPress={() => setPanel(panel === 'frame' ? 'trim' : 'frame')}>
2277
+ <View style={styles.toolIconContainer}>
2278
+ <Ionicons name="images" size={22} color="#fff" />
2279
+ </View>
2280
+ <Text style={styles.toolLabel}>Overlay</Text>
2281
+ </Pressable>
2282
+ <Pressable style={[styles.toolButton, panel === 'edit' && styles.toolButtonActive]} onPress={() => setPanel(panel === 'edit' ? 'trim' : 'edit')}>
2283
+ <View style={styles.toolIconContainer}>
2284
+ <Ionicons name="settings-outline" size={22} color="#fff" />
2285
+ </View>
2286
+ <Text style={styles.toolLabel}>Edit</Text>
2287
+ </Pressable>
2288
+ </ScrollView>
2289
+ </View>
2290
+ </View>
2291
+ ) : (
2292
+ /* Render Preview Mode UI (Screenshot 1) */
2293
+ <View style={[styles.fullPreviewContainer, { justifyContent: 'center', alignItems: 'center' }]}>
2294
+ {/* Floating Close Button */}
2295
+ <Pressable onPress={onBack} style={styles.fullscreenCloseBtn}>
2296
+ <Ionicons name="close" size={24} color="#fff" />
2297
+ </Pressable>
2298
+
2299
+ {/* Floating Sound Toggle */}
2300
+ <Pressable onPress={() => setIsMuted(!isMuted)} style={styles.fullscreenSoundBtn}>
2301
+ <Ionicons name={isMuted ? 'volume-mute' : 'volume-high'} size={22} color="#fff" />
2302
+ </Pressable>
2303
+ <View
2304
+ style={{
2305
+ width: '100%',
2306
+ aspectRatio: videoAspect || 1,
2307
+ maxHeight: '100%',
2308
+ justifyContent: 'center',
2309
+ alignItems: 'center',
2310
+ overflow: 'hidden',
2311
+ backgroundColor: '#000',
2312
+ }}
2313
+ >
2314
+ {/* Fullscreen Video */}
2315
+ <VideoPreview
2316
+ uri={item.uri}
2317
+ paused={videoPaused}
2318
+ muted={isMuted}
2319
+ style={[
2320
+ styles.fullVideo,
2321
+ {
2322
+ position: 'relative',
2323
+ width: '100%',
2324
+ height: '100%',
2325
+ transform: [
2326
+ { scale: zoomScale },
2327
+ { translateX: cropOffset.x },
2328
+ { translateY: cropOffset.y }
2329
+ ]
2330
+ }
2331
+ ]}
2332
+ resizeMode={cropRatio ? "cover" : "contain"}
2333
+ trimStartMs={trimStart}
2334
+ trimEndMs={trimEnd}
2335
+ seekToMs={seekToMs}
2336
+ onChange={(e) => {
2337
+ setCurrentTimeMs(e.nativeEvent.currentTimeMs);
2338
+ }}
2339
+ />
2340
+
2341
+ {/* Text Overlays */}
2342
+ {overlays.map((overlay) => {
2343
+ const responder = getTextResponder(overlay.id);
2344
+ const isSelected = editingTextId === overlay.id;
2345
+ return (
2346
+ <View
2347
+ key={overlay.id}
2348
+ style={[
2349
+ styles.textOverlayContainer,
2350
+ { left: overlay.x, top: overlay.y, zIndex: 20 },
2351
+ isSelected && styles.selectedTextContainer
2352
+ ]}
2353
+ {...responder.panHandlers}
2354
+ >
2355
+ <Pressable
2356
+ onLongPress={() => removeTextOverlay(overlay.id)}
2357
+ onPress={() => {
2358
+ pushToHistory();
2359
+ const found = overlays.find(o => o.id === overlay.id);
2360
+ if (found) {
2361
+ originalOverlayBackup.current = { ...found };
2362
+ }
2363
+ isNewOverlay.current = false;
2364
+ setEditingTextId(overlay.id);
2365
+ setNewText(overlay.text);
2366
+ }}
2367
+ >
2368
+ <Text style={[styles.textOverlay, { color: overlay.color, fontSize: overlay.fontSize }]}>
2369
+ {overlay.text}
2370
+ </Text>
2371
+ </Pressable>
2372
+ </View>
2373
+ );
2374
+ })}
2375
+ </View>
2376
+
2377
+ {/* Fullscreen Bottom Overlay Container */}
2378
+ <View style={styles.fullscreenBottomContainer}>
2379
+ {/* Bottom Toolbar */}
2380
+ <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={[styles.toolButtonsRow, { flexGrow: 1 }]}>
2381
+ <Pressable style={[styles.toolButton, showMusicModal && styles.toolButtonActive]} onPress={() => setShowMusicModal(true)}>
2382
+ <View style={styles.toolIconContainer}>
2383
+ <Ionicons name="musical-notes" size={22} color="#fff" />
2384
+ </View>
2385
+ <Text style={styles.toolLabel}>Audio</Text>
2386
+ </Pressable>
2387
+ <Pressable style={styles.toolButton} onPress={addTextOverlay}>
2388
+ <View style={styles.toolIconContainer}>
2389
+ <Ionicons name="text" size={22} color="#fff" />
2390
+ </View>
2391
+ <Text style={styles.toolLabel}>Text</Text>
2392
+ </Pressable>
2393
+ <Pressable style={[styles.toolButton, panel === 'transform' && styles.toolButtonActive]} onPress={() => { setIsEditingVideo(true); setPanel('transform'); }}>
2394
+ <View style={styles.toolIconContainer}>
2395
+ <Ionicons name="crop" size={22} color="#fff" />
2396
+ </View>
2397
+ <Text style={styles.toolLabel}>Crop</Text>
2398
+ </Pressable>
2399
+ <Pressable style={[styles.toolButton, styles.toolButtonActive]} onPress={() => setIsEditingVideo(true)}>
2400
+ <View style={styles.toolIconContainer}>
2401
+ <Ionicons name="cut" size={22} color="#fff" />
2402
+ </View>
2403
+ <Text style={styles.toolLabel}>Trim</Text>
2404
+ </Pressable>
2405
+ <Pressable style={[styles.toolButton, panel === 'frame' && styles.toolButtonActive]} onPress={() => { setIsEditingVideo(true); setPanel('frame'); }}>
2406
+ <View style={styles.toolIconContainer}>
2407
+ <Ionicons name="images" size={22} color="#fff" />
2408
+ </View>
2409
+ <Text style={styles.toolLabel}>Overlay</Text>
2410
+ </Pressable>
2411
+ <Pressable style={[styles.toolButton, panel === 'filter' && styles.toolButtonActive]} onPress={() => { setIsEditingVideo(true); setPanel('filter'); }}>
2412
+ <View style={styles.toolIconContainer}>
2413
+ <Ionicons name="color-palette" size={22} color="#fff" />
2414
+ </View>
2415
+ <Text style={styles.toolLabel}>Filter</Text>
2416
+ </Pressable>
2417
+ <Pressable style={[styles.toolButton, panel === 'edit' && styles.toolButtonActive]} onPress={() => { setIsEditingVideo(true); setPanel('edit'); }}>
2418
+ <View style={styles.toolIconContainer}>
2419
+ <Ionicons name="settings-outline" size={22} color="#fff" />
2420
+ </View>
2421
+ <Text style={styles.toolLabel}>Edit</Text>
2422
+ </Pressable>
2423
+ </ScrollView>
2424
+
2425
+ {/* Action Row: Edit video & Next */}
2426
+ <View style={styles.fullscreenActionRow}>
2427
+ <Pressable style={styles.editVideoBtn} onPress={() => setIsEditingVideo(true)}>
2428
+ <Text style={styles.editVideoText}>Edit video</Text>
2429
+ </Pressable>
2430
+ <Pressable style={styles.nextPillBtn} onPress={handleSaveAll} disabled={saving}>
2431
+ {saving ? (
2432
+ <ActivityIndicator size="small" color="#fff" />
2433
+ ) : (
2434
+ <Text style={styles.nextPillText}>Next ➔</Text>
2435
+ )}
2436
+ </Pressable>
2437
+ </View>
2438
+ </View>
2439
+ </View>
2440
+ )
2441
+ ) : (
2442
+ <>
2443
+ {/* Modern Header */}
2444
+ <View style={styles.header}>
2445
+ <Pressable onPress={onBack} style={styles.backButton}>
2446
+ <Ionicons name="close" size={22} color="#fff" />
2447
+ </Pressable>
2448
+ <View style={styles.headerRight}>
2449
+ <Pressable onPress={() => setIsMuted(!isMuted)} style={styles.soundButton}>
2450
+ <Ionicons name={isMuted ? 'volume-mute' : 'volume-high'} size={22} color="#fff" />
2451
+ </Pressable>
2452
+ </View>
2453
+ </View>
2454
+
2455
+ <View style={styles.content}>
2456
+ <View style={{ height: carouselHeight, marginVertical: 12 }}>
2457
+ <FlatList
2458
+ ref={flatListRef}
2459
+ data={items}
2460
+ extraData={[
2461
+ overlays,
2462
+ editingTextId,
2463
+ editsHistory,
2464
+ activeFilter,
2465
+ imageOptions,
2466
+ trimStart,
2467
+ trimEnd,
2468
+ isMuted,
2469
+ activeIndex,
2470
+ ]}
2471
+ keyExtractor={(it) => it.id}
2472
+ horizontal
2473
+ pagingEnabled={false}
2474
+ showsHorizontalScrollIndicator={false}
2475
+ snapToInterval={SNAP_INTERVAL}
2476
+ decelerationRate="fast"
2477
+ contentContainerStyle={{
2478
+ paddingHorizontal: (SCREEN_WIDTH - CARD_WIDTH) / 2 - CARD_MARGIN,
2479
+ alignItems: 'center',
2480
+ }}
2481
+ onMomentumScrollEnd={handleScrollEnd}
2482
+ renderItem={renderCard}
2483
+ />
2484
+ </View>
2485
+
2486
+ {selectedMusic && (
2487
+ <Pressable
2488
+ onPress={() => setShowMusicModal(true)}
2489
+ style={styles.floatingMusicBadge}
2490
+ >
2491
+ <Text style={styles.floatingMusicBadgeText}>🎵 {selectedMusic.title} - {selectedMusic.artist}</Text>
2492
+ </Pressable>
2493
+ )}
2494
+
2495
+ <View style={styles.editorPanel}>
2496
+ {panel === 'filter' && (
2497
+ <View style={styles.panelInner}>
2498
+ <Text style={styles.panelTitle}>Filters</Text>
2499
+ <ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.filterScroll}>
2500
+ {Object.keys(FILTERS).map((key) => {
2501
+ const f = (FILTERS as any)[key];
2502
+ return (
2503
+ <Pressable
2504
+ key={key}
2505
+ style={[styles.filterThumb, activeFilter === key && styles.filterThumbActive]}
2506
+ onPress={() => applyFilterPreset(key)}
2507
+ >
2508
+ <View style={styles.filterPreviewContainer}>
2509
+ <Image
2510
+ source={{ uri: item.uri }}
2511
+ style={styles.filterPreview}
2512
+ resizeMode="cover"
2513
+ />
2514
+ <View
2515
+ style={[
2516
+ StyleSheet.absoluteFill,
2517
+ {
2518
+ backgroundColor: getPreviewOverlayColor(key),
2519
+ opacity: getPreviewOverlayOpacity(key),
2520
+ },
2521
+ ]}
2522
+ />
2523
+ </View>
2524
+ <Text style={[styles.filterLabel, activeFilter === key && styles.filterLabelActive]}>
2525
+ {f.label}
2526
+ </Text>
2527
+ </Pressable>
2528
+ );
2529
+ })}
2530
+ </ScrollView>
2531
+ </View>
2532
+ )}
2533
+
2534
+ {panel === 'edit' && (
2535
+ <View style={styles.panelInner}>
2536
+ <Text style={styles.panelTitle}>Adjustments</Text>
2537
+ <ScrollView horizontal showsHorizontalScrollIndicator={false} style={{ marginBottom: 12 }}>
2538
+ <AdjustItem label="Brightness" value={imageOptions.brightness ?? 0} onAdjust={(v) => adjustImage({ brightness: v })} min={-1} max={1} />
2539
+ <AdjustItem label="Contrast" value={imageOptions.contrast ?? 1} onAdjust={(v) => adjustImage({ contrast: v })} min={0} max={2} />
2540
+ <AdjustItem label="Saturation" value={imageOptions.saturation ?? 1} onAdjust={(v) => adjustImage({ saturation: v })} min={0} max={2} />
2541
+ </ScrollView>
2542
+ </View>
2543
+ )}
2544
+
2545
+ {panel === 'trim' && item.type === 'video' && (
2546
+ <View style={styles.panelInner}>
2547
+ <Text style={styles.panelTitle}>Trim Video</Text>
2548
+ <View style={{ alignItems: 'center', marginBottom: 6 }}>
2549
+ <Text style={{ color: '#fff', fontSize: 13, fontWeight: '700' }}>
2550
+ {(() => {
2551
+ const selMs = trimEnd - trimStart;
2552
+ const totalSec = Math.floor(selMs / 1000);
2553
+ return totalSec >= 60
2554
+ ? `${Math.floor(totalSec / 60)}:${(totalSec % 60).toString().padStart(2, '0')} selected`
2555
+ : `${(selMs / 1000).toFixed(1)}s selected`;
2556
+ })()}
2557
+ </Text>
2558
+ </View>
2559
+ <View style={[styles.trimTimelineBox, { overflow: 'visible' }]}>
2560
+ <View style={styles.filmstrip}>
2561
+ {thumbnails.map((uri, idx) => (
2562
+ <Image key={idx} source={{ uri }} style={styles.filmstripImage} />
2563
+ ))}
2564
+ <View style={styles.timelineOverlay} />
2565
+ <View
2566
+ style={[
2567
+ styles.selectionRange,
2568
+ { left: (trimStart / duration) * TIMELINE_WIDTH, width: ((trimEnd - trimStart) / duration) * TIMELINE_WIDTH }
2569
+ ]}
2570
+ />
2571
+ </View>
2572
+ <View
2573
+ style={[
2574
+ styles.customHandle,
2575
+ styles.customHandleLeft,
2576
+ { left: (trimStart / duration) * TIMELINE_WIDTH - 16 }
2577
+ ]}
2578
+ {...startPan.panHandlers}
2579
+ >
2580
+ <View style={styles.handleBarLine} />
2581
+ </View>
2582
+ <View
2583
+ style={[
2584
+ styles.customHandle,
2585
+ styles.customHandleRight,
2586
+ { left: (trimEnd / duration) * TIMELINE_WIDTH }
2587
+ ]}
2588
+ {...endPan.panHandlers}
2589
+ >
2590
+ <View style={styles.handleBarLine} />
2591
+ </View>
2592
+ </View>
2593
+ </View>
2594
+ )}
2595
+
2596
+ {panel === 'frame' && (
2597
+ <View style={styles.panelInner}>
2598
+ <Text style={styles.panelTitle}>Overlays & Frames</Text>
2599
+ <ScrollView horizontal showsHorizontalScrollIndicator={false}>
2600
+ <Pressable
2601
+ style={[styles.filterThumb, !imageOptions.frame && styles.filterThumbActive]}
2602
+ onPress={() => adjustImage({ frame: undefined })}
2603
+ >
2604
+ <View style={[styles.filterPreviewContainer, { backgroundColor: '#333', justifyContent: 'center', alignItems: 'center' }]}>
2605
+ <Ionicons name="close" size={24} color="#fff" />
2606
+ </View>
2607
+ <Text style={[styles.filterLabel, !imageOptions.frame && styles.filterLabelActive]}>None</Text>
2608
+ </Pressable>
2609
+ {FRAME_LIST.map((f) => {
2610
+ const isActive = imageOptions.frame === f.key;
2611
+ return (
2612
+ <Pressable
2613
+ key={f.key}
2614
+ style={[styles.filterThumb, isActive && styles.filterThumbActive]}
2615
+ onPress={() => adjustImage({ frame: f.key })}
2616
+ >
2617
+ <View style={styles.filterPreviewContainer}>
2618
+ {FRAME_IMAGES[f.key] ? (
2619
+ <Image source={FRAME_IMAGES[f.key]} style={{ width: '100%', height: '100%', backgroundColor: '#222' }} resizeMode="stretch" />
2620
+ ) : (
2621
+ <View style={{ width: '100%', height: '100%', backgroundColor: '#222' }} />
2622
+ )}
2623
+ </View>
2624
+ <Text style={[styles.filterLabel, isActive && styles.filterLabelActive]}>{f.label}</Text>
2625
+ </Pressable>
2626
+ );
2627
+ })}
2628
+ </ScrollView>
2629
+ </View>
2630
+ )}
2631
+
2632
+ {panel === 'text' && (
2633
+ <View style={[styles.panelInner, { alignItems: 'center', paddingVertical: 12 }]}>
2634
+ <Pressable style={styles.addTextPill} onPress={addTextOverlay}>
2635
+ <Text style={styles.addTextPillText}>➕ Add Text Box</Text>
2636
+ </Pressable>
2637
+ </View>
2638
+ )}
2639
+
2640
+ {/* ── STICKERS PANEL ────────────────────────────────────────────── */}
2641
+ {panel === 'sticker' && (
2642
+ <View style={styles.panelInner}>
2643
+ <Text style={styles.panelTitle}>Stickers</Text>
2644
+ <ScrollView horizontal={false} style={{ maxHeight: 140 }} showsVerticalScrollIndicator={false}>
2645
+ <View style={{ flexDirection: 'row', flexWrap: 'wrap', paddingHorizontal: 8 }}>
2646
+ {STICKER_LIST.map((emoji, idx) => (
2647
+ <Pressable
2648
+ key={idx}
2649
+ onPress={() => addSticker(emoji)}
2650
+ style={styles.stickerBtn}
2651
+ >
2652
+ <Text style={styles.stickerEmoji}>{emoji}</Text>
2653
+ </Pressable>
2654
+ ))}
2655
+ </View>
2656
+ </ScrollView>
2657
+ {stickers.length > 0 && (
2658
+ <View style={{ flexDirection: 'row', flexWrap: 'wrap', marginTop: 8, paddingHorizontal: 8 }}>
2659
+ <Text style={{ color: '#94a3b8', fontSize: 11, width: '100%', marginBottom: 4 }}>Added — tap to remove:</Text>
2660
+ {stickers.map(s => (
2661
+ <Pressable key={s.id} onPress={() => removeSticker(s.id)} style={styles.addedStickerChip}>
2662
+ <Text style={{ fontSize: 20 }}>{s.emoji}</Text>
2663
+ <Ionicons name="close-circle" size={14} color="#ff6b6b" style={{ marginLeft: 4 }} />
2664
+ </Pressable>
2665
+ ))}
2666
+ </View>
2667
+ )}
2668
+ </View>
2669
+ )}
2670
+
2671
+ {/* ── EFFECTS PANEL ─────────────────────────────────────────────── */}
2672
+ {panel === 'effects' && (
2673
+ <View style={styles.panelInner}>
2674
+ <Text style={styles.panelTitle}>Effects</Text>
2675
+ <ScrollView horizontal showsHorizontalScrollIndicator={false}>
2676
+ {EFFECTS_LIST.map(eff => (
2677
+ <Pressable
2678
+ key={eff.id}
2679
+ style={[styles.effectThumb, activeEffect === eff.id && styles.effectThumbActive]}
2680
+ onPress={() => {
2681
+ pushToHistory();
2682
+ setActiveEffect(eff.id);
2683
+ }}
2684
+ >
2685
+ <View style={[
2686
+ styles.effectIconBox,
2687
+ activeEffect === eff.id && { borderColor: '#3b82f6', backgroundColor: 'rgba(59,130,246,0.2)' }
2688
+ ]}>
2689
+ <Text style={{ fontSize: 26 }}>{eff.icon}</Text>
2690
+ </View>
2691
+ <Text style={[styles.filterLabel, activeEffect === eff.id && styles.filterLabelActive]}>
2692
+ {eff.label}
2693
+ </Text>
2694
+ </Pressable>
2695
+ ))}
2696
+ </ScrollView>
2697
+ </View>
2698
+ )}
2699
+
2700
+ {/* ── CAPTION PANEL ─────────────────────────────────────────────── */}
2701
+ {panel === 'caption' && (
2702
+ <View style={styles.panelInner}>
2703
+ <Text style={styles.panelTitle}>Caption</Text>
2704
+ {/* Style picker */}
2705
+ <ScrollView horizontal showsHorizontalScrollIndicator={false} style={{ marginBottom: 8 }}>
2706
+ {CAPTION_STYLES.map(cs => (
2707
+ <Pressable
2708
+ key={cs.id}
2709
+ style={[styles.captionStylePill, captionStyle === cs.id && styles.captionStylePillActive]}
2710
+ onPress={() => setCaptionStyle(cs.id)}
2711
+ >
2712
+ <Text style={[{ fontSize: 12, fontWeight: cs.fontWeight }, { color: captionStyle === cs.id ? '#fff' : '#94a3b8' }]}>
2713
+ {cs.label}
2714
+ </Text>
2715
+ </Pressable>
2716
+ ))}
2717
+ </ScrollView>
2718
+ {/* Input row */}
2719
+ <View style={styles.captionInputRow}>
2720
+ <TextInput
2721
+ style={styles.captionInput}
2722
+ value={captionInput}
2723
+ onChangeText={setCaptionInput}
2724
+ placeholder="Type caption..."
2725
+ placeholderTextColor="#64748b"
2726
+ returnKeyType="done"
2727
+ onSubmitEditing={addCaption}
2728
+ />
2729
+ <Pressable style={styles.captionAddBtn} onPress={addCaption}>
2730
+ <Text style={{ color: '#fff', fontWeight: '700' }}>Add</Text>
2731
+ </Pressable>
2732
+ </View>
2733
+ {/* Added captions list */}
2734
+ {captions.length > 0 && (
2735
+ <View style={{ marginTop: 8 }}>
2736
+ {captions.map(c => {
2737
+ const cs = CAPTION_STYLES.find(s => s.id === c.style) || CAPTION_STYLES[0];
2738
+ return (
2739
+ <View key={c.id} style={styles.captionListItem}>
2740
+ <View style={[styles.captionPreviewChip, { backgroundColor: cs.bg }]}>
2741
+ <Text style={[styles.captionPreviewText, { color: cs.color, fontWeight: cs.fontWeight }]}>
2742
+ {c.text}
2743
+ </Text>
2744
+ </View>
2745
+ <Pressable onPress={() => removeCaption(c.id)} style={styles.captionRemoveBtn}>
2746
+ <Ionicons name="close" size={14} color="#ff6b6b" />
2747
+ </Pressable>
2748
+ </View>
2749
+ );
2750
+ })}
2751
+ </View>
2752
+ )}
2753
+ </View>
2754
+ )}
2755
+
2756
+ {/* ── ADD CLIP PANEL ────────────────────────────────────────────── */}
2757
+ {panel === 'addclip' && (
2758
+ <View style={[styles.panelInner, { alignItems: 'center', justifyContent: 'center', paddingVertical: 18 }]}>
2759
+ <Pressable style={styles.addClipBigBtn} onPress={() => Alert.alert('Add Clip', 'Clip picker coming soon. You can select another video to append to the timeline.')}>
2760
+ <Text style={{ fontSize: 32, marginBottom: 6 }}>➕</Text>
2761
+ <Text style={styles.addClipBigText}>Add Clip</Text>
2762
+ <Text style={styles.addClipSubText}>Tap to pick another video</Text>
2763
+ </Pressable>
2764
+ </View>
2765
+ )}
2766
+ </View>
2767
+
2768
+ {/* Tools row and bottom navigation controls */}
2769
+ <View style={styles.bottomToolBarContainer}>
2770
+ <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={[styles.toolButtonsRow, { flexGrow: 1 }]}>
2771
+ <Pressable style={[styles.toolButton, showMusicModal && styles.toolButtonActive]} onPress={() => setShowMusicModal(true)}>
2772
+ <View style={styles.toolIconContainer}>
2773
+ <Ionicons name="musical-notes" size={22} color="#fff" />
2774
+ </View>
2775
+ <Text style={styles.toolLabel}>Audio</Text>
2776
+ </Pressable>
2777
+ <Pressable style={[styles.toolButton, panel === 'text' && styles.toolButtonActive]} onPress={() => setPanel('text')}>
2778
+ <View style={styles.toolIconContainer}>
2779
+ <Ionicons name="text" size={22} color="#fff" />
2780
+ </View>
2781
+ <Text style={styles.toolLabel}>Text</Text>
2782
+ </Pressable>
2783
+ {item.type === 'image' ? (
2784
+ <Pressable style={[styles.toolButton]} onPress={handleOpenTransform}>
2785
+ <View style={styles.toolIconContainer}>
2786
+ <Ionicons name="crop" size={22} color="#fff" />
2787
+ </View>
2788
+ <Text style={styles.toolLabel}>Crop</Text>
2789
+ </Pressable>
2790
+ ) : (
2791
+ <Pressable style={[styles.toolButton, panel === 'trim' && styles.toolButtonActive]} onPress={() => setPanel('trim')}>
2792
+ <View style={styles.toolIconContainer}>
2793
+ <Ionicons name="cut" size={22} color="#fff" />
2794
+ </View>
2795
+ <Text style={styles.toolLabel}>Trim</Text>
2796
+ </Pressable>
2797
+ )}
2798
+ <Pressable style={[styles.toolButton, panel === 'frame' && styles.toolButtonActive]} onPress={() => setPanel('frame')}>
2799
+ <View style={styles.toolIconContainer}>
2800
+ <Ionicons name="images" size={22} color="#fff" />
2801
+ </View>
2802
+ <Text style={styles.toolLabel}>Overlay</Text>
2803
+ </Pressable>
2804
+ <Pressable style={[styles.toolButton, panel === 'filter' && styles.toolButtonActive]} onPress={() => setPanel('filter')}>
2805
+ <View style={styles.toolIconContainer}>
2806
+ <Ionicons name="color-palette" size={22} color="#fff" />
2807
+ </View>
2808
+ <Text style={styles.toolLabel}>Filter</Text>
2809
+ </Pressable>
2810
+ <Pressable style={[styles.toolButton, panel === 'edit' && styles.toolButtonActive]} onPress={() => setPanel('edit')}>
2811
+ <View style={styles.toolIconContainer}>
2812
+ <Ionicons name="settings-outline" size={22} color="#fff" />
2813
+ </View>
2814
+ <Text style={styles.toolLabel}>Edit</Text>
2815
+ </Pressable>
2816
+ </ScrollView>
2817
+
2818
+ <View style={styles.bottomNavButtonsRow}>
2819
+ <Pressable style={styles.nextBlueBtn} onPress={handleSaveAll} disabled={saving}>
2820
+ {saving ? (
2821
+ <ActivityIndicator size="small" color="#fff" />
2822
+ ) : (
2823
+ <View style={{ flexDirection: 'row', alignItems: 'center' }}>
2824
+ <Text style={styles.nextBlueText}>Next</Text>
2825
+ <Ionicons name="arrow-forward" size={16} color="#fff" style={{ marginLeft: 4 }} />
2826
+ </View>
2827
+ )}
2828
+ </Pressable>
2829
+ </View>
2830
+ </View>
2831
+ </View>
2832
+ </>
2833
+ )}
2834
+
2835
+ <Modal
2836
+ visible={showMusicModal}
2837
+ transparent
2838
+ animationType="slide"
2839
+ onRequestClose={() => setShowMusicModal(false)}
2840
+ >
2841
+ <Pressable style={styles.musicModalOverlay} onPress={() => setShowMusicModal(false)}>
2842
+ <View style={styles.musicModalContent} onStartShouldSetResponder={() => true} onTouchEnd={(e) => e.stopPropagation()}>
2843
+ {/* Top Handle Bar */}
2844
+ <View style={styles.musicModalHandle} />
2845
+
2846
+ {/* Header */}
2847
+ <View style={styles.musicModalHeader}>
2848
+ <Text style={styles.musicModalTitle}>Select Music</Text>
2849
+ <Pressable onPress={() => setShowMusicModal(false)} style={styles.musicModalCloseBtn}>
2850
+ <Ionicons name="close" size={24} color="#fff" />
2851
+ </Pressable>
2852
+ </View>
2853
+
2854
+ {/* Search Input */}
2855
+ <View style={styles.musicSearchContainer}>
2856
+ <Text style={styles.musicSearchIcon}>🔍</Text>
2857
+ <TextInput
2858
+ style={styles.musicSearchInput}
2859
+ value={musicSearchQuery}
2860
+ onChangeText={setMusicSearchQuery}
2861
+ placeholder="Search..."
2862
+ placeholderTextColor="#8e8e93"
2863
+ autoCapitalize="none"
2864
+ />
2865
+ {musicSearchQuery.length > 0 && (
2866
+ <Pressable onPress={() => setMusicSearchQuery('')} style={styles.musicSearchClearBtn}>
2867
+ <Ionicons name="close-circle" size={18} color="#8e8e93" />
2868
+ </Pressable>
2869
+ )}
2870
+ </View>
2871
+
2872
+ {/* Tabs */}
2873
+ <View style={styles.musicTabsContainer}>
2874
+ <ScrollView horizontal showsHorizontalScrollIndicator={false}>
2875
+ {[
2876
+ { id: 'for_you', label: 'For you' },
2877
+ { id: 'trending', label: 'Trending' },
2878
+ { id: 'saved', label: 'Saved' },
2879
+ { id: 'original', label: 'Original audio' },
2880
+ { id: 'custom', label: 'Custom music' }
2881
+ ].map((tab) => {
2882
+ const isActive = activeMusicTab === tab.id;
2883
+ return (
2884
+ <Pressable
2885
+ key={tab.id}
2886
+ onPress={() => setActiveMusicTab(tab.id as any)}
2887
+ style={[styles.musicTabPill, isActive && styles.musicTabPillActive]}
2888
+ >
2889
+ <Text style={[styles.musicTabLabel, isActive && styles.musicTabLabelActive]}>
2890
+ {tab.label}
2891
+ </Text>
2892
+ </Pressable>
2893
+ );
2894
+ })}
2895
+ </ScrollView>
2896
+ </View>
2897
+
2898
+ {/* Music List */}
2899
+ <FlatList
2900
+ data={filteredMusicList}
2901
+ keyExtractor={(item) => item.id}
2902
+ showsVerticalScrollIndicator={false}
2903
+ contentContainerStyle={{ paddingBottom: 40 }}
2904
+ renderItem={({ item: track, index }) => {
2905
+ const isSelected = selectedMusic?.id === track.id;
2906
+ const startIndex = activeMusicTab === 'trending' ? 14 : 1;
2907
+ return (
2908
+ <Pressable
2909
+ onPress={() => {
2910
+ if (isSelected) {
2911
+ setMusicPaused(!musicPaused);
2912
+ } else {
2913
+ setSelectedMusic(track);
2914
+ setMusicPaused(false);
2915
+ // Auto-mute video audio when music is selected
2916
+ setIsMuted(true);
2917
+ }
2918
+ }}
2919
+ style={[styles.musicRow, isSelected && styles.musicRowSelected]}
2920
+ >
2921
+ <Text style={styles.musicRowIndex}>{index + startIndex}</Text>
2922
+ <Image source={{ uri: track.cover }} style={styles.musicRowCover} />
2923
+ <View style={styles.musicRowInfo}>
2924
+ <Text style={styles.musicRowTitle} numberOfLines={1}>
2925
+ {track.title}
2926
+ </Text>
2927
+ <Text style={styles.musicRowArtist} numberOfLines={1}>
2928
+ {track.artist} • {track.duration}
2929
+ </Text>
2930
+ </View>
2931
+ {isSelected && (
2932
+ <View style={styles.musicPlayingIndicator}>
2933
+ <Text style={{ fontSize: 11, color: '#38bdf8', marginRight: 12, fontWeight: '700' }}>
2934
+ {musicPaused ? 'PAUSED' : 'PLAYING'}
2935
+ </Text>
2936
+ </View>
2937
+ )}
2938
+ <Pressable style={styles.musicSaveBtn}>
2939
+ <Text style={styles.musicSaveIcon}>🔖</Text>
2940
+ </Pressable>
2941
+ </Pressable>
2942
+ );
2943
+ }}
2944
+ ListEmptyComponent={
2945
+ <View style={styles.musicListEmpty}>
2946
+ <Text style={styles.musicListEmptyText}>No music tracks found</Text>
2947
+ </View>
2948
+ }
2949
+ />
2950
+
2951
+ {/* Selected Music Footer */}
2952
+ {selectedMusic && (
2953
+ <View style={styles.musicModalFooter}>
2954
+ <View style={styles.musicFooterLeft}>
2955
+ <Image source={{ uri: selectedMusic.cover }} style={styles.musicFooterCover} />
2956
+ <View style={{ flex: 1 }}>
2957
+ <Text style={styles.musicFooterTitle} numberOfLines={1}>
2958
+ {selectedMusic.title}
2959
+ </Text>
2960
+ <Text style={styles.musicFooterArtist} numberOfLines={1}>
2961
+ {selectedMusic.artist}
2962
+ </Text>
2963
+ </View>
2964
+ </View>
2965
+ <Pressable
2966
+ onPress={() => {
2967
+ setSelectedMusic(null);
2968
+ setMusicPaused(true);
2969
+ }}
2970
+ style={styles.musicFooterRemoveBtn}
2971
+ >
2972
+ <Text style={styles.musicFooterRemoveText}>Remove</Text>
2973
+ </Pressable>
2974
+ </View>
2975
+ )}
2976
+ </View>
2977
+ </Pressable>
2978
+ </Modal>
2979
+
2980
+ <Modal visible={!!editingTextId} transparent animationType="fade">
2981
+ <View style={styles.modalOverlay}>
2982
+ <View style={styles.modalContent}>
2983
+ <Text style={styles.controlSubTitle}>ADD TEXT</Text>
2984
+ <TextInput
2985
+ style={[
2986
+ styles.modalInput,
2987
+ { color: overlays.find(o => o.id === editingTextId)?.color || '#FFFFFF' }
2988
+ ]}
2989
+ value={newText}
2990
+ onChangeText={(txt) => {
2991
+ setNewText(txt);
2992
+ if (editingTextId) {
2993
+ updateTextOverlay(editingTextId, { text: txt });
2994
+ }
2995
+ }}
2996
+ autoFocus
2997
+ placeholder="Type something..."
2998
+ placeholderTextColor="#555"
2999
+ />
3000
+
3001
+ <View style={styles.modalColorPicker}>
3002
+ <Text style={styles.controlSubTitle}>Pick Color</Text>
3003
+ <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ paddingHorizontal: 4 }}>
3004
+ {[
3005
+ '#FFFFFF', '#000000', '#FF3B30', '#FF9500', '#FFCC00',
3006
+ '#4CD964', '#5AC8FA', '#007AFF', '#5856D6', '#FF2D55',
3007
+ '#A2845E', '#E5E5EA', '#8E8E93', '#3A3A3C'
3008
+ ].map(c => (
3009
+ <Pressable
3010
+ key={c}
3011
+ onPress={() => updateTextOverlay(editingTextId!, { color: c })}
3012
+ style={[styles.colorOption, { backgroundColor: c }, overlays.find(o => o.id === editingTextId)?.color === c && styles.colorOptionActive]}
3013
+ />
3014
+ ))}
3015
+ </ScrollView>
3016
+ </View>
3017
+
3018
+ <View style={styles.modalActions}>
3019
+ <Pressable
3020
+ onPress={() => {
3021
+ if (editingTextId) {
3022
+ if (isNewOverlay.current) {
3023
+ setUndoStack(prev => prev.slice(0, prev.length - 1));
3024
+ setOverlays(prev => prev.filter(o => o.id !== editingTextId));
3025
+ } else if (originalOverlayBackup.current) {
3026
+ setUndoStack(prev => prev.slice(0, prev.length - 1));
3027
+ setOverlays(prev => prev.map(o => o.id === editingTextId ? { ...o, ...originalOverlayBackup.current } : o));
3028
+ }
3029
+ }
3030
+ isNewOverlay.current = false;
3031
+ originalOverlayBackup.current = null;
3032
+ setEditingTextId(null);
3033
+ setNewText('');
3034
+ }}
3035
+ style={styles.modalBtn}
3036
+ >
3037
+ <Text style={styles.modalBtnText}>Cancel</Text>
3038
+ </Pressable>
3039
+ <Pressable
3040
+ onPress={() => {
3041
+ if (editingTextId) {
3042
+ if (!newText.trim()) {
3043
+ setOverlays(prev => prev.filter(o => o.id !== editingTextId));
3044
+ } else {
3045
+ setOverlays(prev => prev.map(o => o.id === editingTextId ? { ...o, text: newText.trim() } : o));
3046
+ }
3047
+ }
3048
+ isNewOverlay.current = false;
3049
+ originalOverlayBackup.current = null;
3050
+ setEditingTextId(null);
3051
+ setNewText('');
3052
+ }}
3053
+ style={[styles.modalBtn, styles.modalBtnPrimary]}
3054
+ >
3055
+ <Text style={styles.modalBtnTextPrimary}>Done</Text>
3056
+ </Pressable>
3057
+ </View>
3058
+ </View>
3059
+ </View>
3060
+ </Modal>
3061
+
3062
+ </View>
3063
+ );
3064
+ }
3065
+
3066
+ const styles = StyleSheet.create({
3067
+ container: { flex: 1, backgroundColor: '#000000' },
3068
+ fullPreviewContainer: {
3069
+ flex: 1,
3070
+ backgroundColor: '#000',
3071
+ },
3072
+ fullVideo: {
3073
+ position: 'absolute',
3074
+ top: 0,
3075
+ left: 0,
3076
+ right: 0,
3077
+ bottom: 0,
3078
+ },
3079
+ fullscreenCloseBtn: {
3080
+ position: 'absolute',
3081
+ top: Platform.OS === 'ios' ? 54 : 16,
3082
+ left: 16,
3083
+ width: 40,
3084
+ height: 40,
3085
+ borderRadius: 20,
3086
+ backgroundColor: 'rgba(0,0,0,0.5)',
3087
+ justifyContent: 'center',
3088
+ alignItems: 'center',
3089
+ zIndex: 10,
3090
+ },
3091
+ fullscreenCloseText: {
3092
+ color: '#FFF',
3093
+ fontSize: 18,
3094
+ fontWeight: '600',
3095
+ },
3096
+ fullscreenSoundBtn: {
3097
+ position: 'absolute',
3098
+ top: Platform.OS === 'ios' ? 54 : 16,
3099
+ right: 16,
3100
+ width: 40,
3101
+ height: 40,
3102
+ borderRadius: 20,
3103
+ backgroundColor: 'rgba(0,0,0,0.5)',
3104
+ justifyContent: 'center',
3105
+ alignItems: 'center',
3106
+ zIndex: 10,
3107
+ },
3108
+ fullscreenSoundText: {
3109
+ fontSize: 18,
3110
+ },
3111
+ fullscreenBottomContainer: {
3112
+ position: 'absolute',
3113
+ bottom: 0,
3114
+ left: 0,
3115
+ right: 0,
3116
+ paddingBottom: 36,
3117
+ paddingTop: 20,
3118
+ backgroundColor: '#000000',
3119
+ zIndex: 10,
3120
+ },
3121
+ fullscreenActionRow: {
3122
+ flexDirection: 'row',
3123
+ justifyContent: 'space-between',
3124
+ alignItems: 'center',
3125
+ paddingHorizontal: 20,
3126
+ marginTop: 16,
3127
+ },
3128
+ editVideoBtn: {
3129
+ paddingVertical: 12,
3130
+ paddingHorizontal: 24,
3131
+ borderRadius: 24,
3132
+ borderWidth: 1.2,
3133
+ borderColor: '#FFF',
3134
+ backgroundColor: 'rgba(0,0,0,0.3)',
3135
+ },
3136
+ editVideoText: {
3137
+ color: '#FFF',
3138
+ fontSize: 15,
3139
+ fontWeight: '700',
3140
+ },
3141
+ nextPillBtn: {
3142
+ paddingVertical: 12,
3143
+ paddingHorizontal: 30,
3144
+ borderRadius: 24,
3145
+ backgroundColor: '#FFFFFF',
3146
+ },
3147
+ nextPillText: {
3148
+ color: '#000',
3149
+ fontSize: 15,
3150
+ fontWeight: '700',
3151
+ },
3152
+ editModeContainer: {
3153
+ flex: 1,
3154
+ backgroundColor: '#000000',
3155
+ },
3156
+ editModeHeader: {
3157
+ flexDirection: 'row',
3158
+ justifyContent: 'space-between',
3159
+ alignItems: 'center',
3160
+ paddingHorizontal: 16,
3161
+ height: 56,
3162
+ marginTop: 12,
3163
+ },
3164
+ editModeBackBtn: {
3165
+ width: 40,
3166
+ height: 40,
3167
+ borderRadius: 20,
3168
+ backgroundColor: 'rgba(255,255,255,0.15)',
3169
+ justifyContent: 'center',
3170
+ alignItems: 'center',
3171
+ },
3172
+ editModeBackText: {
3173
+ color: '#FFF',
3174
+ fontSize: 20,
3175
+ fontWeight: 'bold',
3176
+ textAlign: 'center',
3177
+ includeFontPadding: false,
3178
+ textAlignVertical: 'center',
3179
+ },
3180
+ editModeNextBtn: {
3181
+ width: 40,
3182
+ height: 40,
3183
+ borderRadius: 20,
3184
+ backgroundColor: '#3b82f6',
3185
+ justifyContent: 'center',
3186
+ alignItems: 'center',
3187
+ },
3188
+
3189
+ editModeNextText: {
3190
+ color: '#FFF',
3191
+ fontSize: 20,
3192
+ fontWeight: 'bold',
3193
+ textAlign: 'center',
3194
+ includeFontPadding: false, // Android
3195
+ },
3196
+ editModePlayerContainer: {
3197
+ flex: 1,
3198
+ justifyContent: 'center',
3199
+ alignItems: 'center',
3200
+ backgroundColor: '#000',
3201
+ position: 'relative',
3202
+ },
3203
+ editModeVideo: {
3204
+ width: '100%',
3205
+ height: '100%',
3206
+ },
3207
+ playerControlsRow: {
3208
+ flexDirection: 'row',
3209
+ alignItems: 'center',
3210
+ paddingHorizontal: 20,
3211
+ paddingVertical: 10,
3212
+ backgroundColor: '#000',
3213
+ },
3214
+ editPlayPauseBtn: {
3215
+ width: 36,
3216
+ height: 36,
3217
+ borderRadius: 18,
3218
+ backgroundColor: 'rgba(255,255,255,0.2)',
3219
+ justifyContent: 'center',
3220
+ alignItems: 'center',
3221
+ },
3222
+ playPauseIconText: {
3223
+ color: '#FFF',
3224
+ fontSize: 16,
3225
+ },
3226
+ handleText: {
3227
+ color: '#FFF',
3228
+ fontSize: 12,
3229
+ fontWeight: 'bold',
3230
+ },
3231
+ timelinePlayhead: {
3232
+ position: 'absolute',
3233
+ top: 0,
3234
+ bottom: 0,
3235
+ width: 3,
3236
+ backgroundColor: '#FF3B30',
3237
+ zIndex: 15,
3238
+ },
3239
+ durationText: {
3240
+ color: '#FFF',
3241
+ marginLeft: 12,
3242
+ fontSize: 13,
3243
+ fontWeight: '600',
3244
+ },
3245
+ historyButtons: {
3246
+ flexDirection: 'row',
3247
+ marginLeft: 'auto',
3248
+ },
3249
+ historyBtn: {
3250
+ width: 32,
3251
+ height: 32,
3252
+ justifyContent: 'center',
3253
+ alignItems: 'center',
3254
+ marginHorizontal: 4,
3255
+ },
3256
+ historyBtnText: {
3257
+ color: '#FFF',
3258
+ fontSize: 18,
3259
+ },
3260
+ timelineSection: {
3261
+ backgroundColor: '#111111',
3262
+ paddingVertical: 12,
3263
+ },
3264
+ timelineRuler: {
3265
+ flexDirection: 'row',
3266
+ justifyContent: 'space-around',
3267
+ paddingHorizontal: 0,
3268
+ marginBottom: 8,
3269
+ },
3270
+ timelineRulerDot: {
3271
+ width: 2,
3272
+ height: 6,
3273
+ backgroundColor: '#333333',
3274
+ },
3275
+ timelineRulerText: {
3276
+ color: '#555555',
3277
+ fontSize: 10,
3278
+ },
3279
+ timelineScrollContainer: {
3280
+ position: 'relative',
3281
+ paddingVertical: 8,
3282
+ },
3283
+ timelineCenterLine: {
3284
+ position: 'absolute',
3285
+ left: SCREEN_WIDTH / 2,
3286
+ top: 12,
3287
+ bottom: 32,
3288
+ width: 2,
3289
+ backgroundColor: '#FFFFFF',
3290
+ zIndex: 100,
3291
+ },
3292
+ timelineTracksContainer: {
3293
+ paddingHorizontal: 0,
3294
+ },
3295
+ filmstripTrack: {
3296
+ height: 60,
3297
+ borderRadius: 8,
3298
+ overflow: 'hidden',
3299
+ backgroundColor: '#222222',
3300
+ marginBottom: 12,
3301
+ },
3302
+ subTrackRow: {
3303
+ flexDirection: 'row',
3304
+ alignItems: 'center',
3305
+ height: 38,
3306
+ borderRadius: 6,
3307
+ backgroundColor: '#1E1E1E',
3308
+ paddingHorizontal: 12,
3309
+ marginBottom: 8,
3310
+ },
3311
+ subTrackIcon: {
3312
+ color: '#007AFF',
3313
+ fontSize: 16,
3314
+ fontWeight: '700',
3315
+ marginRight: 8,
3316
+ },
3317
+ subTrackText: {
3318
+ color: '#FFFFFF',
3319
+ fontSize: 12,
3320
+ fontWeight: '600',
3321
+ },
3322
+ timelineHintText: {
3323
+ color: '#444444',
3324
+ fontSize: 11,
3325
+ textAlign: 'center',
3326
+ marginTop: 4,
3327
+ },
3328
+ musicCard: {
3329
+ width: 110,
3330
+ backgroundColor: '#1E1E1E',
3331
+ borderRadius: 8,
3332
+ padding: 8,
3333
+ marginRight: 12,
3334
+ alignItems: 'center',
3335
+ borderWidth: 1.5,
3336
+ borderColor: 'transparent',
3337
+ },
3338
+ musicCardActive: {
3339
+ borderColor: '#007AFF',
3340
+ backgroundColor: '#2A2A2A',
3341
+ },
3342
+ musicIconContainer: {
3343
+ width: 50,
3344
+ height: 50,
3345
+ borderRadius: 25,
3346
+ backgroundColor: '#333',
3347
+ justifyContent: 'center',
3348
+ alignItems: 'center',
3349
+ marginBottom: 8,
3350
+ },
3351
+ musicIconContainerActive: {
3352
+ backgroundColor: '#007AFF',
3353
+ },
3354
+ musicTitle: {
3355
+ color: '#FFF',
3356
+ fontSize: 12,
3357
+ fontWeight: '600',
3358
+ textAlign: 'center',
3359
+ width: '100%',
3360
+ },
3361
+ musicArtist: {
3362
+ color: '#AAA',
3363
+ fontSize: 10,
3364
+ textAlign: 'center',
3365
+ marginTop: 2,
3366
+ width: '100%',
3367
+ },
3368
+ musicDuration: {
3369
+ color: '#666',
3370
+ fontSize: 9,
3371
+ marginTop: 4,
3372
+ },
3373
+ playPauseBtn: {
3374
+ marginTop: 6,
3375
+ backgroundColor: '#007AFF',
3376
+ paddingHorizontal: 8,
3377
+ paddingVertical: 3,
3378
+ borderRadius: 4,
3379
+ },
3380
+ header: {
3381
+ height: 56,
3382
+ flexDirection: 'row',
3383
+ alignItems: 'center',
3384
+ justifyContent: 'space-between',
3385
+ paddingHorizontal: 16,
3386
+ borderBottomWidth: 0.5,
3387
+ borderBottomColor: '#333',
3388
+ },
3389
+ headerIcon: { color: '#fff', fontSize: 24 },
3390
+ headerLink: { color: '#fff', fontSize: 16, fontWeight: '600' },
3391
+ title: { color: '#fff', fontSize: 16, fontWeight: '700' },
3392
+ headerRight: { flexDirection: 'row', alignItems: 'center' },
3393
+ downloadIcon: { marginRight: 20 },
3394
+ content: { flex: 1 },
3395
+ previewContainer: {
3396
+ height: SCREEN_WIDTH * 1.25,
3397
+ backgroundColor: '#111',
3398
+ justifyContent: 'center',
3399
+ alignItems: 'center',
3400
+ padding: 12,
3401
+ },
3402
+ previewBox: {
3403
+ width: '100%',
3404
+ height: '100%',
3405
+ borderRadius: 12,
3406
+ overflow: 'hidden',
3407
+ alignSelf: 'center',
3408
+ },
3409
+ preview: { width: '100%', height: '100%' },
3410
+ previewContainerResized: {
3411
+ width: '100%',
3412
+ height: '100%',
3413
+ position: 'absolute',
3414
+ top: 0,
3415
+ left: 0,
3416
+ bottom: 0,
3417
+ right: 0
3418
+ },
3419
+ videoPreview: {
3420
+ flex: 1,
3421
+ alignItems: 'center',
3422
+ justifyContent: 'center',
3423
+ backgroundColor: '#000',
3424
+ },
3425
+ previewOverlay: {
3426
+ ...StyleSheet.absoluteFillObject,
3427
+ alignItems: 'center',
3428
+ justifyContent: 'center',
3429
+ },
3430
+ quickRatioWrap: {
3431
+ position: 'absolute',
3432
+ left: 12,
3433
+ bottom: 12,
3434
+ zIndex: 20,
3435
+ },
3436
+ quickRatioButton: {
3437
+ flexDirection: 'row',
3438
+ alignItems: 'center',
3439
+ gap: 8,
3440
+ backgroundColor: 'rgba(20,24,33,0.95)',
3441
+ borderRadius: 16,
3442
+ paddingHorizontal: 12,
3443
+ paddingVertical: 10,
3444
+ },
3445
+ quickRatioButtonText: { color: '#fff', fontSize: 16, fontWeight: '600' },
3446
+ quickRatioMenu: {
3447
+ marginBottom: 8,
3448
+ backgroundColor: 'rgba(20,24,33,0.96)',
3449
+ borderRadius: 14,
3450
+ overflow: 'hidden',
3451
+ minWidth: 150,
3452
+ },
3453
+ quickRatioItem: {
3454
+ flexDirection: 'row',
3455
+ alignItems: 'center',
3456
+ paddingHorizontal: 12,
3457
+ paddingVertical: 12,
3458
+ },
3459
+ quickRatioIcon: { color: '#fff', width: 18, fontSize: 16 },
3460
+ quickRatioText: { color: '#fff', fontSize: 16, flex: 1, fontWeight: '500' },
3461
+ quickRatioCheck: { color: '#fff', fontSize: 18, fontWeight: '700' },
3462
+ playPauseCircle: {
3463
+ width: 64,
3464
+ height: 64,
3465
+ borderRadius: 32,
3466
+ backgroundColor: 'rgba(0,0,0,0.4)',
3467
+ alignItems: 'center',
3468
+ justifyContent: 'center',
3469
+ },
3470
+ playPauseText: { color: '#fff', fontSize: 24, marginLeft: 4 },
3471
+ editorPanel: { flex: 1, justifyContent: 'center' },
3472
+ panelInner: { paddingHorizontal: 16 },
3473
+ panelTitle: { color: '#999', fontSize: 12, fontWeight: '700', marginBottom: 16, textTransform: 'uppercase' },
3474
+ bottomNav: {
3475
+ height: 80,
3476
+ borderTopWidth: 0.5,
3477
+ borderTopColor: '#333',
3478
+ justifyContent: 'center',
3479
+ },
3480
+ tabsBar: {
3481
+ flexDirection: 'row',
3482
+ justifyContent: 'space-around',
3483
+ paddingHorizontal: 12,
3484
+ },
3485
+ trimBottomBar: {
3486
+ flexDirection: 'row',
3487
+ justifyContent: 'space-between',
3488
+ alignItems: 'center',
3489
+ paddingHorizontal: 20,
3490
+ },
3491
+ bottomLinkText: { color: '#fff', fontSize: 16, fontWeight: '500' },
3492
+ doneText: { color: '#38bdf8' },
3493
+ disabled: { opacity: 0.5 },
3494
+ trayButton: {
3495
+ paddingVertical: 10,
3496
+ paddingHorizontal: 20,
3497
+ alignItems: 'center',
3498
+ },
3499
+ trayButtonActive: { borderBottomWidth: 2, borderBottomColor: '#fff' },
3500
+ trayButtonDisabled: { opacity: 0.4 },
3501
+ trayButtonText: { color: '#fff', fontWeight: '700', fontSize: 13 },
3502
+ filterScroll: { marginTop: 4 },
3503
+ filterThumb: {
3504
+ width: 80,
3505
+ marginRight: 12,
3506
+ alignItems: 'center',
3507
+ },
3508
+ filterThumbActive: { transform: [{ scale: 1.05 }] },
3509
+ filterPreviewContainer: {
3510
+ width: 70,
3511
+ height: 70,
3512
+ borderRadius: 8,
3513
+ overflow: 'hidden',
3514
+ marginBottom: 8,
3515
+ },
3516
+ filterPreview: { width: '100%', height: '100%' },
3517
+ filterLabel: { color: '#999', fontSize: 11, fontWeight: '500' },
3518
+ filterLabelActive: { color: '#fff', fontWeight: '700' },
3519
+
3520
+ colorCircle: {
3521
+ width: 48,
3522
+ height: 48,
3523
+ borderRadius: 24,
3524
+ marginRight: 12,
3525
+ alignItems: 'center',
3526
+ justifyContent: 'center',
3527
+ },
3528
+ colorCircleActive: {
3529
+ transform: [{ scale: 1.1 }],
3530
+ borderWidth: 2,
3531
+ borderColor: '#fff',
3532
+ },
3533
+
3534
+ frameSwatch: {
3535
+ alignItems: 'center',
3536
+ marginRight: 12,
3537
+ },
3538
+ frameSwatchBox: {
3539
+ width: 52,
3540
+ height: 52,
3541
+ borderRadius: 10,
3542
+ alignItems: 'center',
3543
+ justifyContent: 'center',
3544
+ marginBottom: 6,
3545
+ },
3546
+ frameSwatchBoxActive: {
3547
+ borderWidth: 2.5,
3548
+ borderColor: '#38bdf8',
3549
+ },
3550
+ frameSwatchLabel: {
3551
+ color: '#666',
3552
+ fontSize: 11,
3553
+ fontWeight: '500',
3554
+ },
3555
+
3556
+ adjustItem: {
3557
+ width: 100,
3558
+ alignItems: 'center',
3559
+ marginRight: 16,
3560
+ },
3561
+ adjustLabel: { color: '#999', fontSize: 12, marginBottom: 12 },
3562
+ adjustCircle: {
3563
+ width: 48,
3564
+ height: 48,
3565
+ borderRadius: 24,
3566
+ backgroundColor: '#111',
3567
+ alignItems: 'center',
3568
+ justifyContent: 'center',
3569
+ borderWidth: 1,
3570
+ borderColor: '#333',
3571
+ },
3572
+ adjustCircleActive: { borderColor: '#fff' },
3573
+ adjustValue: { color: '#fff', fontSize: 10, fontWeight: '700' },
3574
+ adjustSmallButton: {
3575
+ width: 24,
3576
+ height: 24,
3577
+ borderRadius: 12,
3578
+ backgroundColor: '#222',
3579
+ alignItems: 'center',
3580
+ justifyContent: 'center',
3581
+ marginHorizontal: 4,
3582
+ },
3583
+ adjustSmallButtonText: { color: '#fff', fontSize: 14, fontWeight: '700' },
3584
+ trimTimelineBox: { width: TIMELINE_WIDTH, alignSelf: 'center', marginTop: 10 },
3585
+ filmstrip: {
3586
+ width: '100%',
3587
+ height: 60,
3588
+ flexDirection: 'row',
3589
+ backgroundColor: '#111',
3590
+ borderRadius: 4,
3591
+ overflow: 'hidden',
3592
+ },
3593
+ filmstripImage: { width: TIMELINE_WIDTH / 10, height: 60 },
3594
+ timelineOverlay: {
3595
+ ...StyleSheet.absoluteFillObject,
3596
+ backgroundColor: 'rgba(0,0,0,0.6)',
3597
+ },
3598
+ selectionRange: {
3599
+ position: 'absolute',
3600
+ top: 0,
3601
+ height: 60,
3602
+ backgroundColor: 'transparent',
3603
+ borderTopWidth: 2,
3604
+ borderBottomWidth: 2,
3605
+ borderColor: '#fff',
3606
+ },
3607
+ handle: {
3608
+ position: 'absolute',
3609
+ top: 18,
3610
+ width: HANDLE_SIZE,
3611
+ height: HANDLE_SIZE,
3612
+ borderRadius: HANDLE_SIZE / 2,
3613
+ backgroundColor: '#fff',
3614
+ zIndex: 10,
3615
+ elevation: 5,
3616
+ shadowColor: '#000',
3617
+ shadowOffset: { width: 0, height: 2 },
3618
+ shadowOpacity: 0.3,
3619
+ shadowRadius: 2,
3620
+ },
3621
+ handleLeft: {},
3622
+ handleRight: {},
3623
+ customHandle: {
3624
+ position: 'absolute',
3625
+ top: 0,
3626
+ width: 16,
3627
+ height: 60,
3628
+ backgroundColor: '#FFFFFF',
3629
+ justifyContent: 'center',
3630
+ alignItems: 'center',
3631
+ zIndex: 20,
3632
+ },
3633
+ customHandleLeft: {
3634
+ borderTopLeftRadius: 8,
3635
+ borderBottomLeftRadius: 8,
3636
+ },
3637
+ customHandleRight: {
3638
+ borderTopRightRadius: 8,
3639
+ borderBottomRightRadius: 8,
3640
+ },
3641
+ handleBarLine: {
3642
+ width: 2,
3643
+ height: 16,
3644
+ backgroundColor: '#333333',
3645
+ borderRadius: 1,
3646
+ },
3647
+ timeMarkers: {
3648
+ flexDirection: 'row',
3649
+ justifyContent: 'space-between',
3650
+ marginTop: 8,
3651
+ paddingHorizontal: 2,
3652
+ },
3653
+ timeCode: { color: '#666', fontSize: 10, fontWeight: '500' },
3654
+ row: { flexDirection: 'row', flexWrap: 'wrap', marginBottom: 8 },
3655
+ aspectItem: {
3656
+ alignItems: 'center',
3657
+ marginRight: 20,
3658
+ width: 60,
3659
+ },
3660
+ aspectIconContainer: {
3661
+ height: 48,
3662
+ alignItems: 'center',
3663
+ justifyContent: 'center',
3664
+ marginBottom: 8,
3665
+ },
3666
+ resetIcon: {
3667
+ color: '#fff',
3668
+ fontSize: 24,
3669
+ },
3670
+ aspectDashed: {
3671
+ width: 32,
3672
+ height: 32,
3673
+ borderWidth: 1,
3674
+ borderColor: '#999',
3675
+ borderStyle: 'dashed',
3676
+ },
3677
+ aspectBox: {
3678
+ width: 32,
3679
+ backgroundColor: '#666',
3680
+ },
3681
+ aspectActiveBorder: {
3682
+ borderColor: '#fff',
3683
+ backgroundColor: 'transparent',
3684
+ borderWidth: 2,
3685
+ },
3686
+ aspectLabel: {
3687
+ color: '#999',
3688
+ fontSize: 12,
3689
+ },
3690
+ aspectLabelActive: {
3691
+ color: '#fff',
3692
+ fontWeight: '700',
3693
+ },
3694
+ // Transform Tool Styles
3695
+ cropOverlay: {
3696
+ ...StyleSheet.absoluteFillObject,
3697
+ justifyContent: 'center',
3698
+ alignItems: 'center',
3699
+ },
3700
+ gridLineV: {
3701
+ position: 'absolute',
3702
+ left: '33.3%',
3703
+ width: 0.8,
3704
+ height: '100%',
3705
+ backgroundColor: 'rgba(255,255,255,0.7)',
3706
+ },
3707
+ gridLineV2: {
3708
+ position: 'absolute',
3709
+ left: '66.6%',
3710
+ width: 0.8,
3711
+ height: '100%',
3712
+ backgroundColor: 'rgba(255,255,255,0.7)',
3713
+ },
3714
+ gridLineH: {
3715
+ position: 'absolute',
3716
+ top: '33.3%',
3717
+ height: 0.8,
3718
+ width: '100%',
3719
+ backgroundColor: 'rgba(255,255,255,0.7)',
3720
+ },
3721
+ gridLineH2: {
3722
+ position: 'absolute',
3723
+ top: '66.6%',
3724
+ height: 0.8,
3725
+ width: '100%',
3726
+ backgroundColor: 'rgba(255,255,255,0.7)',
3727
+ },
3728
+ corner: {
3729
+ position: 'absolute',
3730
+ width: 44,
3731
+ height: 44,
3732
+ borderColor: '#fff',
3733
+ backgroundColor: 'transparent',
3734
+ zIndex: 20,
3735
+ },
3736
+ cornerTL: { top: -4, left: -4, borderTopWidth: 4, borderLeftWidth: 4 },
3737
+ cornerTR: { top: -4, right: -4, borderTopWidth: 4, borderRightWidth: 4 },
3738
+ cornerBL: { bottom: -4, left: -4, borderBottomWidth: 4, borderLeftWidth: 4 },
3739
+ cornerBR: { bottom: -4, right: -4, borderBottomWidth: 4, borderRightWidth: 4 },
3740
+
3741
+ rotationRuler: {
3742
+ flexDirection: 'row',
3743
+ alignItems: 'center',
3744
+ justifyContent: 'space-between',
3745
+ paddingHorizontal: 20,
3746
+ marginBottom: 24,
3747
+ height: 60,
3748
+ },
3749
+ rulerIcon: { color: '#fff', fontSize: 22 },
3750
+ rulerCenter: { flex: 1, alignItems: 'center' },
3751
+ rulerValue: { color: '#fff', fontSize: 13, fontWeight: '700', marginBottom: 10 },
3752
+ rulerMarks: { flexDirection: 'row', alignItems: 'flex-end', height: 20 },
3753
+ rulerMark: { width: 1, height: 8, backgroundColor: '#444', marginHorizontal: 3 },
3754
+ rulerMarkActive: { height: 16, backgroundColor: '#888' },
3755
+
3756
+ transformBottomBar: {
3757
+ flexDirection: 'row',
3758
+ alignItems: 'center',
3759
+ justifyContent: 'space-between',
3760
+ paddingHorizontal: 16,
3761
+ height: '100%',
3762
+ },
3763
+ bottomBarIconBtn: { padding: 10 },
3764
+ bottomBarIcon: { color: '#fff', fontSize: 20, fontWeight: '700' },
3765
+ bottomBarTitle: { color: '#fff', fontSize: 13, fontWeight: '700', letterSpacing: 1.2 },
3766
+
3767
+ // Text Overlay Styles
3768
+ textOverlayContainer: {
3769
+ position: 'absolute',
3770
+ padding: 8,
3771
+ zIndex: 30,
3772
+ },
3773
+ textOverlay: {
3774
+ fontWeight: '700',
3775
+ },
3776
+ selectedTextContainer: {
3777
+ borderWidth: 1,
3778
+ borderColor: '#38bdf8',
3779
+ borderStyle: 'dashed',
3780
+ borderRadius: 4,
3781
+ },
3782
+ textActionRow: {
3783
+ flexDirection: 'row',
3784
+ justifyContent: 'center',
3785
+ marginBottom: 16,
3786
+ },
3787
+ addTextBtn: {
3788
+ backgroundColor: '#38bdf8',
3789
+ paddingHorizontal: 20,
3790
+ paddingVertical: 10,
3791
+ borderRadius: 20,
3792
+ },
3793
+ addTextBtnText: {
3794
+ color: '#fff',
3795
+ fontWeight: '700',
3796
+ fontSize: 14,
3797
+ },
3798
+ textEditControls: {
3799
+ marginTop: 10,
3800
+ },
3801
+ controlSubTitle: {
3802
+ color: '#999',
3803
+ fontSize: 12,
3804
+ fontWeight: '600',
3805
+ marginBottom: 10,
3806
+ textTransform: 'uppercase',
3807
+ },
3808
+ colorOption: {
3809
+ width: 36,
3810
+ height: 36,
3811
+ borderRadius: 18,
3812
+ marginRight: 12,
3813
+ borderWidth: 2,
3814
+ borderColor: 'transparent',
3815
+ },
3816
+ colorOptionActive: {
3817
+ borderColor: '#38bdf8',
3818
+ transform: [{ scale: 1.1 }],
3819
+ },
3820
+ textEmptyState: {
3821
+ alignItems: 'center',
3822
+ padding: 20,
3823
+ },
3824
+ emptyStateText: {
3825
+ color: '#555',
3826
+ fontSize: 14,
3827
+ textAlign: 'center',
3828
+ },
3829
+ fontSizeRow: {
3830
+ flexDirection: 'row',
3831
+ alignItems: 'center',
3832
+ marginTop: 20,
3833
+ justifyContent: 'center',
3834
+ },
3835
+ fontSizeLabel: {
3836
+ color: '#999',
3837
+ marginRight: 15,
3838
+ fontSize: 14,
3839
+ },
3840
+ sizeBtn: {
3841
+ width: 36,
3842
+ height: 36,
3843
+ borderRadius: 18,
3844
+ backgroundColor: '#222',
3845
+ alignItems: 'center',
3846
+ justifyContent: 'center',
3847
+ },
3848
+ sizeBtnText: {
3849
+ color: '#fff',
3850
+ fontSize: 20,
3851
+ fontWeight: '700',
3852
+ },
3853
+ sizeValue: {
3854
+ color: '#fff',
3855
+ fontSize: 16,
3856
+ fontWeight: '700',
3857
+ marginHorizontal: 20,
3858
+ minWidth: 30,
3859
+ textAlign: 'center',
3860
+ },
3861
+ modalOverlay: {
3862
+ flex: 1,
3863
+ backgroundColor: 'rgba(0,0,0,0.85)',
3864
+ justifyContent: 'center',
3865
+ alignItems: 'center',
3866
+ padding: 20,
3867
+ },
3868
+ modalContent: {
3869
+ width: '100%',
3870
+ backgroundColor: '#1a1a1a',
3871
+ borderRadius: 15,
3872
+ padding: 20,
3873
+ alignItems: 'center',
3874
+ },
3875
+ modalInput: {
3876
+ width: '100%',
3877
+ color: '#fff',
3878
+ fontSize: 24,
3879
+ fontWeight: '700',
3880
+ textAlign: 'center',
3881
+ paddingVertical: 20,
3882
+ borderBottomWidth: 1,
3883
+ borderBottomColor: '#333',
3884
+ marginBottom: 20,
3885
+ },
3886
+ modalColorPicker: {
3887
+ width: '100%',
3888
+ marginBottom: 20,
3889
+ },
3890
+ modalActions: {
3891
+ flexDirection: 'row',
3892
+ marginTop: 20,
3893
+ justifyContent: 'flex-end',
3894
+ width: '100%',
3895
+ },
3896
+ modalBtn: {
3897
+ paddingHorizontal: 20,
3898
+ paddingVertical: 10,
3899
+ marginLeft: 10,
3900
+ },
3901
+ modalBtnPrimary: {
3902
+ backgroundColor: '#38bdf8',
3903
+ borderRadius: 8,
3904
+ },
3905
+ modalBtnText: {
3906
+ color: '#999',
3907
+ fontSize: 16,
3908
+ fontWeight: '600',
3909
+ },
3910
+ modalBtnTextPrimary: {
3911
+ color: '#fff',
3912
+ fontSize: 16,
3913
+ fontWeight: '700',
3914
+ },
3915
+ floatingMusicBadge: {
3916
+ position: 'absolute',
3917
+ top: 20,
3918
+ right: 20,
3919
+ backgroundColor: 'rgba(0, 0, 0, 0.75)',
3920
+ borderRadius: 16,
3921
+ paddingHorizontal: 12,
3922
+ paddingVertical: 6,
3923
+ flexDirection: 'row',
3924
+ alignItems: 'center',
3925
+ zIndex: 30,
3926
+ },
3927
+ floatingMusicBadgeText: {
3928
+ color: '#FFF',
3929
+ fontSize: 12,
3930
+ fontWeight: '600',
3931
+ },
3932
+ musicModalOverlay: {
3933
+ flex: 1,
3934
+ backgroundColor: 'rgba(0, 0, 0, 0)',
3935
+ justifyContent: 'flex-end',
3936
+ },
3937
+ musicModalContent: {
3938
+ backgroundColor: '#12141C',
3939
+ borderTopLeftRadius: 24,
3940
+ borderTopRightRadius: 24,
3941
+ height: '50%',
3942
+ paddingTop: 12,
3943
+ },
3944
+ musicModalHandle: {
3945
+ width: 36,
3946
+ height: 4.5,
3947
+ borderRadius: 2.5,
3948
+ backgroundColor: '#2C303E',
3949
+ alignSelf: 'center',
3950
+ marginBottom: 16,
3951
+ },
3952
+ musicModalHeader: {
3953
+ flexDirection: 'row',
3954
+ justifyContent: 'space-between',
3955
+ alignItems: 'center',
3956
+ paddingHorizontal: 20,
3957
+ marginBottom: 16,
3958
+ },
3959
+ musicModalTitle: {
3960
+ color: '#FFFFFF',
3961
+ fontSize: 20,
3962
+ fontWeight: '700',
3963
+ },
3964
+ musicModalCloseBtn: {
3965
+ padding: 6,
3966
+ },
3967
+ musicModalCloseText: {
3968
+ color: '#A2A2A5',
3969
+ fontSize: 16,
3970
+ fontWeight: '600',
3971
+ },
3972
+ musicSearchContainer: {
3973
+ flexDirection: 'row',
3974
+ alignItems: 'center',
3975
+ backgroundColor: '#1C1F2E',
3976
+ borderRadius: 12,
3977
+ marginHorizontal: 16,
3978
+ paddingHorizontal: 12,
3979
+ height: 40,
3980
+ marginBottom: 16,
3981
+ },
3982
+ musicSearchIcon: {
3983
+ fontSize: 14,
3984
+ marginRight: 8,
3985
+ color: '#7D8395',
3986
+ },
3987
+ musicSearchInput: {
3988
+ flex: 1,
3989
+ color: '#FFFFFF',
3990
+ fontSize: 15,
3991
+ padding: 0,
3992
+ },
3993
+ musicSearchClearBtn: {
3994
+ padding: 4,
3995
+ },
3996
+ musicSearchClearText: {
3997
+ color: '#8E93A2',
3998
+ fontSize: 12,
3999
+ fontWeight: '700',
4000
+ },
4001
+ musicTabsContainer: {
4002
+ paddingHorizontal: 16,
4003
+ marginBottom: 16,
4004
+ },
4005
+ musicTabPill: {
4006
+ paddingHorizontal: 16,
4007
+ paddingVertical: 8,
4008
+ borderRadius: 20,
4009
+ backgroundColor: '#1C1F2E',
4010
+ marginRight: 8,
4011
+ },
4012
+ musicTabPillActive: {
4013
+ backgroundColor: '#FFFFFF',
4014
+ },
4015
+ musicTabLabel: {
4016
+ color: '#8E93A2',
4017
+ fontSize: 13,
4018
+ fontWeight: '600',
4019
+ },
4020
+ musicTabLabelActive: {
4021
+ color: '#000000',
4022
+ },
4023
+ musicRow: {
4024
+ flexDirection: 'row',
4025
+ alignItems: 'center',
4026
+ paddingVertical: 12,
4027
+ paddingHorizontal: 18,
4028
+ },
4029
+ musicRowSelected: {
4030
+ backgroundColor: '#1E2436',
4031
+ },
4032
+ musicRowIndex: {
4033
+ color: '#525866',
4034
+ fontSize: 13,
4035
+ fontWeight: '600',
4036
+ width: 24,
4037
+ },
4038
+ musicRowCover: {
4039
+ width: 44,
4040
+ height: 44,
4041
+ borderRadius: 8,
4042
+ marginRight: 12,
4043
+ },
4044
+ musicRowInfo: {
4045
+ flex: 1,
4046
+ justifyContent: 'center',
4047
+ },
4048
+ musicRowTitle: {
4049
+ color: '#FFFFFF',
4050
+ fontSize: 15,
4051
+ fontWeight: '600',
4052
+ marginBottom: 2,
4053
+ },
4054
+ musicRowArtist: {
4055
+ color: '#8E93A2',
4056
+ fontSize: 12,
4057
+ },
4058
+ musicSaveBtn: {
4059
+ padding: 8,
4060
+ },
4061
+ musicSaveIcon: {
4062
+ fontSize: 16,
4063
+ },
4064
+ musicPlayingIndicator: {
4065
+ marginRight: 10,
4066
+ backgroundColor: 'rgba(56, 189, 248, 0.1)',
4067
+ paddingHorizontal: 8,
4068
+ paddingVertical: 4,
4069
+ borderRadius: 4,
4070
+ },
4071
+ musicListEmpty: {
4072
+ alignItems: 'center',
4073
+ justifyContent: 'center',
4074
+ paddingVertical: 40,
4075
+ },
4076
+ musicListEmptyText: {
4077
+ color: '#8E93A2',
4078
+ fontSize: 14,
4079
+ },
4080
+ musicModalFooter: {
4081
+ flexDirection: 'row',
4082
+ justifyContent: 'space-between',
4083
+ alignItems: 'center',
4084
+ paddingHorizontal: 16,
4085
+ paddingVertical: 12,
4086
+ backgroundColor: '#12141C',
4087
+ borderTopWidth: 1,
4088
+ borderTopColor: '#1D202F',
4089
+ paddingBottom: 24,
4090
+ },
4091
+ musicFooterLeft: {
4092
+ flexDirection: 'row',
4093
+ alignItems: 'center',
4094
+ flex: 1,
4095
+ marginRight: 16,
4096
+ },
4097
+ musicFooterCover: {
4098
+ width: 36,
4099
+ height: 36,
4100
+ borderRadius: 6,
4101
+ marginRight: 10,
4102
+ },
4103
+ musicFooterTitle: {
4104
+ color: '#FFFFFF',
4105
+ fontSize: 13,
4106
+ fontWeight: '600',
4107
+ },
4108
+ musicFooterArtist: {
4109
+ color: '#8E93A2',
4110
+ fontSize: 11,
4111
+ marginTop: 1,
4112
+ },
4113
+ musicFooterRemoveBtn: {
4114
+ backgroundColor: 'rgba(239, 68, 68, 0.1)',
4115
+ paddingHorizontal: 12,
4116
+ paddingVertical: 6,
4117
+ borderRadius: 14,
4118
+ },
4119
+ musicFooterRemoveText: {
4120
+ color: '#EF4444',
4121
+ fontSize: 12,
4122
+ fontWeight: '600',
4123
+ },
4124
+ backButton: {
4125
+ padding: 10,
4126
+ },
4127
+ backButtonText: {
4128
+ color: '#FFFFFF',
4129
+ fontSize: 20,
4130
+ fontWeight: '600',
4131
+ },
4132
+ soundButton: {
4133
+ padding: 10,
4134
+ backgroundColor: 'rgba(255, 255, 255, 0.1)',
4135
+ borderRadius: 20,
4136
+ marginRight: 10,
4137
+ },
4138
+ soundIconText: {
4139
+ fontSize: 18,
4140
+ },
4141
+ cardContainer: {
4142
+ justifyContent: 'center',
4143
+ alignItems: 'center',
4144
+ },
4145
+ cardPreviewBox: {
4146
+ shadowColor: '#000',
4147
+ shadowOffset: { width: 0, height: 4 },
4148
+ shadowOpacity: 0.3,
4149
+ shadowRadius: 8,
4150
+ elevation: 5,
4151
+ },
4152
+ cropIconBtn: {
4153
+ position: 'absolute',
4154
+ bottom: 12,
4155
+ left: 12,
4156
+ backgroundColor: 'rgba(0, 0, 0, 0.6)',
4157
+ width: 36,
4158
+ height: 36,
4159
+ borderRadius: 18,
4160
+ justifyContent: 'center',
4161
+ alignItems: 'center',
4162
+ zIndex: 30,
4163
+ borderWidth: 1,
4164
+ borderColor: 'rgba(255, 255, 255, 0.3)',
4165
+ },
4166
+ cropIconText: {
4167
+ color: '#FFFFFF',
4168
+ fontSize: 18,
4169
+ fontWeight: 'bold',
4170
+ },
4171
+ bottomToolBarContainer: {
4172
+ backgroundColor: '#0A0A0C',
4173
+ borderTopWidth: 1,
4174
+ borderTopColor: '#1A1A1E',
4175
+ paddingBottom: Platform.OS === 'ios' ? 24 : 12,
4176
+ },
4177
+ toolButtonsRow: {
4178
+ flexDirection: 'row',
4179
+ justifyContent: 'space-around',
4180
+ paddingVertical: 10,
4181
+ paddingHorizontal: 4,
4182
+ borderBottomWidth: 1,
4183
+ borderBottomColor: '#1A1A1E',
4184
+ },
4185
+ toolButton: {
4186
+ alignItems: 'center',
4187
+ justifyContent: 'center',
4188
+ width: 52,
4189
+ },
4190
+ toolButtonActive: {
4191
+ transform: [{ scale: 1.05 }],
4192
+ },
4193
+ toolIconContainer: {
4194
+ width: 42,
4195
+ height: 42,
4196
+ borderRadius: 8,
4197
+ backgroundColor: 'rgba(255, 255, 255, 0.08)',
4198
+ justifyContent: 'center',
4199
+ alignItems: 'center',
4200
+ marginBottom: 4,
4201
+ },
4202
+ toolIconText: {
4203
+ fontSize: 15,
4204
+ color: '#FFFFFF',
4205
+ fontWeight: '600',
4206
+ textAlign: 'center',
4207
+ textAlignVertical: 'center',
4208
+ includeFontPadding: false,
4209
+ },
4210
+ toolLabel: {
4211
+ fontSize: 9,
4212
+ color: '#FFFFFF',
4213
+ fontWeight: '500',
4214
+ textAlign: 'center',
4215
+ },
4216
+ bottomNavButtonsRow: {
4217
+ flexDirection: 'row',
4218
+ justifyContent: 'flex-end',
4219
+ alignItems: 'center',
4220
+ paddingHorizontal: 20,
4221
+ paddingVertical: 12,
4222
+ },
4223
+ addMediaButton: {
4224
+ position: 'relative',
4225
+ },
4226
+ addMediaThumb: {
4227
+ width: 44,
4228
+ height: 44,
4229
+ borderRadius: 22,
4230
+ borderWidth: 1.5,
4231
+ borderColor: '#38bdf8',
4232
+ },
4233
+ addMediaPlusBadge: {
4234
+ position: 'absolute',
4235
+ bottom: -3,
4236
+ right: -3,
4237
+ backgroundColor: '#38bdf8',
4238
+ width: 18,
4239
+ height: 18,
4240
+ borderRadius: 9,
4241
+ justifyContent: 'center',
4242
+ alignItems: 'center',
4243
+ borderWidth: 1.5,
4244
+ borderColor: '#000',
4245
+ },
4246
+ addMediaPlusText: {
4247
+ color: '#FFF',
4248
+ fontSize: 11,
4249
+ fontWeight: 'bold',
4250
+ },
4251
+ nextBlueBtn: {
4252
+ backgroundColor: '#007AFF',
4253
+ paddingVertical: 10,
4254
+ paddingHorizontal: 24,
4255
+ borderRadius: 22,
4256
+ shadowColor: '#007AFF',
4257
+ shadowOffset: { width: 0, height: 4 },
4258
+ shadowOpacity: 0.3,
4259
+ shadowRadius: 6,
4260
+ elevation: 3,
4261
+ },
4262
+ nextBlueText: {
4263
+ color: '#FFF',
4264
+ fontSize: 14,
4265
+ fontWeight: '700',
4266
+ },
4267
+ addTextPill: {
4268
+ flexDirection: 'row',
4269
+ alignItems: 'center',
4270
+ backgroundColor: 'rgba(56, 189, 248, 0.1)',
4271
+ borderWidth: 1,
4272
+ borderColor: '#38bdf8',
4273
+ paddingVertical: 8,
4274
+ paddingHorizontal: 16,
4275
+ borderRadius: 20,
4276
+ },
4277
+ addTextPillText: {
4278
+ color: '#38bdf8',
4279
+ fontSize: 13,
4280
+ fontWeight: '700',
4281
+ },
4282
+
4283
+ // ── Stickers ────────────────────────────────────────────────────────────────
4284
+ stickerBtn: {
4285
+ width: 52,
4286
+ height: 52,
4287
+ borderRadius: 12,
4288
+ backgroundColor: 'rgba(255,255,255,0.07)',
4289
+ alignItems: 'center',
4290
+ justifyContent: 'center',
4291
+ margin: 4,
4292
+ },
4293
+ stickerEmoji: {
4294
+ fontSize: 28,
4295
+ },
4296
+ addedStickerChip: {
4297
+ flexDirection: 'row',
4298
+ alignItems: 'center',
4299
+ backgroundColor: 'rgba(255,255,255,0.1)',
4300
+ borderRadius: 20,
4301
+ paddingHorizontal: 8,
4302
+ paddingVertical: 4,
4303
+ marginRight: 6,
4304
+ marginBottom: 4,
4305
+ },
4306
+
4307
+ // ── Effects ─────────────────────────────────────────────────────────────────
4308
+ effectThumb: {
4309
+ alignItems: 'center',
4310
+ marginHorizontal: 6,
4311
+ opacity: 0.7,
4312
+ },
4313
+ effectThumbActive: {
4314
+ opacity: 1,
4315
+ },
4316
+ effectIconBox: {
4317
+ width: 62,
4318
+ height: 62,
4319
+ borderRadius: 14,
4320
+ backgroundColor: 'rgba(255,255,255,0.08)',
4321
+ borderWidth: 2,
4322
+ borderColor: 'transparent',
4323
+ alignItems: 'center',
4324
+ justifyContent: 'center',
4325
+ marginBottom: 4,
4326
+ },
4327
+
4328
+ // ── Captions ─────────────────────────────────────────────────────────────────
4329
+ captionStylePill: {
4330
+ paddingHorizontal: 14,
4331
+ paddingVertical: 6,
4332
+ borderRadius: 20,
4333
+ borderWidth: 1,
4334
+ borderColor: '#334155',
4335
+ marginRight: 8,
4336
+ backgroundColor: 'rgba(255,255,255,0.05)',
4337
+ },
4338
+ captionStylePillActive: {
4339
+ borderColor: '#3b82f6',
4340
+ backgroundColor: 'rgba(59,130,246,0.2)',
4341
+ },
4342
+ captionInputRow: {
4343
+ flexDirection: 'row',
4344
+ alignItems: 'center',
4345
+ backgroundColor: 'rgba(255,255,255,0.07)',
4346
+ borderRadius: 12,
4347
+ paddingHorizontal: 10,
4348
+ paddingVertical: 4,
4349
+ gap: 8,
4350
+ },
4351
+ captionInput: {
4352
+ flex: 1,
4353
+ color: '#fff',
4354
+ fontSize: 14,
4355
+ paddingVertical: 6,
4356
+ },
4357
+ captionAddBtn: {
4358
+ backgroundColor: '#3b82f6',
4359
+ paddingHorizontal: 16,
4360
+ paddingVertical: 8,
4361
+ borderRadius: 10,
4362
+ },
4363
+ captionListItem: {
4364
+ flexDirection: 'row',
4365
+ alignItems: 'center',
4366
+ marginBottom: 6,
4367
+ gap: 8,
4368
+ },
4369
+ captionPreviewChip: {
4370
+ flex: 1,
4371
+ paddingHorizontal: 12,
4372
+ paddingVertical: 6,
4373
+ borderRadius: 8,
4374
+ },
4375
+ captionPreviewText: {
4376
+ fontSize: 13,
4377
+ },
4378
+ captionRemoveBtn: {
4379
+ width: 28,
4380
+ height: 28,
4381
+ borderRadius: 14,
4382
+ backgroundColor: 'rgba(255,100,100,0.15)',
4383
+ alignItems: 'center',
4384
+ justifyContent: 'center',
4385
+ },
4386
+
4387
+ // ── Add Clip ─────────────────────────────────────────────────────────────────
4388
+ addClipBigBtn: {
4389
+ alignItems: 'center',
4390
+ justifyContent: 'center',
4391
+ backgroundColor: 'rgba(59,130,246,0.12)',
4392
+ borderWidth: 2,
4393
+ borderColor: '#3b82f6',
4394
+ borderStyle: 'dashed',
4395
+ borderRadius: 20,
4396
+ paddingVertical: 20,
4397
+ paddingHorizontal: 40,
4398
+ },
4399
+ addClipBigText: {
4400
+ color: '#3b82f6',
4401
+ fontSize: 16,
4402
+ fontWeight: '700',
4403
+ marginBottom: 2,
4404
+ },
4405
+ addClipSubText: {
4406
+ color: '#64748b',
4407
+ fontSize: 12,
4408
+ },
4409
+ });
4410
+
4411
+ function TrayButton({ label, onPress, disabled, active }: { label: string; onPress: () => void; disabled?: boolean; active?: boolean }) {
4412
+ return (
4413
+ <Pressable
4414
+ style={[styles.trayButton, active && styles.trayButtonActive, disabled && styles.trayButtonDisabled]}
4415
+ onPress={onPress}
4416
+ disabled={disabled}
4417
+ >
4418
+ <Text style={styles.trayButtonText}>{label}</Text>
4419
+ </Pressable>
4420
+ );
4421
+ }
4422
+
4423
+ function AdjustItem({
4424
+ label,
4425
+ value,
4426
+ onAdjust,
4427
+ min = 0,
4428
+ max = 2,
4429
+ isToggle,
4430
+ isAction,
4431
+ icon,
4432
+ }: {
4433
+ label: string;
4434
+ value: number;
4435
+ onAdjust: (v: number) => void;
4436
+ min?: number;
4437
+ max?: number;
4438
+ isToggle?: boolean;
4439
+ isAction?: boolean;
4440
+ icon?: string;
4441
+ }) {
4442
+ const handleIncrease = () => {
4443
+ if (isToggle || isAction) {
4444
+ onAdjust(value);
4445
+ } else {
4446
+ onAdjust(Math.min(max, value + 0.1));
4447
+ }
4448
+ };
4449
+
4450
+ const handleDecrease = () => {
4451
+ if (isToggle || isAction) {
4452
+ onAdjust(value);
4453
+ } else {
4454
+ onAdjust(Math.max(min, value - 0.1));
4455
+ }
4456
+ };
4457
+
4458
+ const isChanged = () => {
4459
+ if (isToggle) return value === 1;
4460
+ if (isAction) return false;
4461
+ if (label === 'Contrast' || label === 'Saturation') return Math.abs(value - 1) > 0.01;
4462
+ return Math.abs(value) > 0.01;
4463
+ };
4464
+
4465
+ return (
4466
+ <View style={styles.adjustItem}>
4467
+ <Text style={styles.adjustLabel}>{label}</Text>
4468
+ <View style={styles.row}>
4469
+ {!isToggle && !isAction && (
4470
+ <Pressable style={styles.adjustSmallButton} onPress={handleDecrease}>
4471
+ <Text style={styles.adjustSmallButtonText}>-</Text>
4472
+ </Pressable>
4473
+ )}
4474
+ <Pressable
4475
+ style={[styles.adjustCircle, isChanged() && styles.adjustCircleActive]}
4476
+ onPress={handleIncrease}
4477
+ >
4478
+ {icon ? (
4479
+ <Text style={[styles.adjustValue, { fontSize: 18 }]}>{icon}</Text>
4480
+ ) : (
4481
+ <Text style={styles.adjustValue}>
4482
+ {isToggle ? (value ? 'ON' : 'OFF') : value.toFixed(1)}
4483
+ </Text>
4484
+ )}
4485
+ </Pressable>
4486
+ {!isToggle && !isAction && (
4487
+ <Pressable style={styles.adjustSmallButton} onPress={handleIncrease}>
4488
+ <Text style={styles.adjustSmallButtonText}>+</Text>
4489
+ </Pressable>
4490
+ )}
4491
+ </View>
4492
+ </View>
4493
+ );
4494
+ }
4495
+
4496
+ function AspectRatioItem({ label, active, onPress, width, height, isReset }: any) {
4497
+ return (
4498
+ <Pressable style={styles.aspectItem} onPress={onPress}>
4499
+ <View style={styles.aspectIconContainer}>
4500
+ {isReset ? (
4501
+ <Text style={styles.resetIcon}>↺</Text>
4502
+ ) : label === 'Custom' ? (
4503
+ <View style={[styles.aspectDashed, active && styles.aspectActiveBorder]} />
4504
+ ) : label === 'Square' ? (
4505
+ <View style={[styles.aspectBox, { aspectRatio: 1 }, active && styles.aspectActiveBorder]} />
4506
+ ) : label === '16:9' ? (
4507
+ <View style={[styles.aspectBox, { aspectRatio: 16 / 9, justifyContent: 'center', alignItems: 'center' }, active && styles.aspectActiveBorder]}>
4508
+ <Text style={{ color: active ? '#fff' : '#999', fontSize: 10 }}>↔</Text>
4509
+ </View>
4510
+ ) : (
4511
+ <View style={[styles.aspectBox, { aspectRatio: (width || 1) / (height || 1) }, active && styles.aspectActiveBorder]} />
4512
+ )}
4513
+ </View>
4514
+ <Text style={[styles.aspectLabel, active && styles.aspectLabelActive]}>{label}</Text>
4515
+ </Pressable>
4516
+ );
4517
+ }