Sunflower UI
Search
Copy Code
Light Mode

Turks and Caicos

Watch on YouTube
0:00 / 0:00
video.tsx
prompt.md
Collapse
Reset
Copy Code
Download
Run
1
'use client';
2
3
import React, { useState, useRef, useEffect, useCallback } from 'react';
4
import { motion, AnimatePresence } from 'framer-motion';
5
import {
6
Play, Pause, Volume2, VolumeX, Maximize, Minimize,
7
RotateCcw, RotateCw, PictureInPicture, Youtube
8
} from 'lucide-react';
9
import { cn } from '@/lib/utils';
10
11
export interface VideoPlayerProps {
12
src: string;
13
title?: string;
14
youtubeUrl?: string;
15
className?: string;
16
}
17
18
function formatTime(timeInSeconds: number): string {
19
if (isNaN(timeInSeconds)) return '0:00';
20
const minutes = Math.floor(timeInSeconds / 60);
21
const seconds = Math.floor(timeInSeconds % 60);
22
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
23
}
24
25
export function VideoPlayer({ src, title, youtubeUrl, className }: VideoPlayerProps) {
26
const [isPlaying, setIsPlaying] = useState(false);
27
const [currentTime, setCurrentTime] = useState(0);
28
const [duration, setDuration] = useState(0);
29
const [volume, setVolume] = useState(1);
30
const [isMuted, setIsMuted] = useState(false);
31
const [isFullscreen, setIsFullscreen] = useState(false);
32
const [playbackRate, setPlaybackRate] = useState(1);
33
const [showControls, setShowControls] = useState(true);
34
const [isScrubbing, setIsScrubbing] = useState(false);
35
36
const videoRef = useRef<HTMLVideoElement>(null);
37
const containerRef = useRef<HTMLDivElement>(null);
38
const progressRef = useRef<HTMLDivElement>(null);
39
const controlsTimeoutRef = useRef<NodeJS.Timeout>(null);
40
41
// Auto-hide controls
42
const handleMouseMove = useCallback(() => {
43
setShowControls(true);
44
if (controlsTimeoutRef.current) clearTimeout(controlsTimeoutRef.current);
45
46
if (isPlaying) {
47
controlsTimeoutRef.current = setTimeout(() => {
48
if (!isScrubbing) setShowControls(false);
49
}, 2500);
50
}
51
}, [isPlaying, isScrubbing]);
52
53
const handleMouseLeave = () => {
54
if (isPlaying && !isScrubbing) {
55
setShowControls(false);
56
}
57
};
58
59
useEffect(() => {
60
return () => {
61
if (controlsTimeoutRef.current) clearTimeout(controlsTimeoutRef.current);
62
};
63
}, []);
64
65
// Video Events
66
const togglePlay = (e?: React.MouseEvent) => {
67
e?.stopPropagation();
68
if (videoRef.current) {
69
if (isPlaying) {
70
videoRef.current.pause();
71
} else {
72
videoRef.current.play();
73
}
74
setIsPlaying(!isPlaying);
75
}
76
};
77
78
const handleTimeUpdate = () => {
79
if (videoRef.current && !isScrubbing) {
80
setCurrentTime(videoRef.current.currentTime);
81
}
82
};
83
84
const handleLoadedMetadata = () => {
85
if (videoRef.current) {
86
setDuration(videoRef.current.duration);
87
}
88
};
89
90
const skip = (amount: number, e: React.MouseEvent) => {
91
e.stopPropagation();
92
if (videoRef.current) {
93
videoRef.current.currentTime += amount;
94
setCurrentTime(videoRef.current.currentTime);
95
}
96
};
97
98
// Scrubbing
99
const handleScrub = (e: React.MouseEvent | MouseEvent) => {
100
if (progressRef.current && videoRef.current) {
101
const rect = progressRef.current.getBoundingClientRect();
102
const pos = (e.clientX - rect.left) / rect.width;
103
const boundedPos = Math.max(0, Math.min(1, pos));
104
const newTime = boundedPos * duration;
105
setCurrentTime(newTime);
106
videoRef.current.currentTime = newTime;
107
}
108
};
109
110
const handleProgressMouseDown = (e: React.MouseEvent) => {
111
e.stopPropagation();
112
setIsScrubbing(true);
113
handleScrub(e);
114
};
115
116
useEffect(() => {
117
const handleMouseUp = () => setIsScrubbing(false);
118
const handleMouseMoveGlobal = (e: MouseEvent) => {
119
if (isScrubbing) handleScrub(e);
120
};
121
122
if (isScrubbing) {
123
window.addEventListener('mousemove', handleMouseMoveGlobal);
124
window.addEventListener('mouseup', handleMouseUp);
125
}
126
127
return () => {
128
window.removeEventListener('mousemove', handleMouseMoveGlobal);
129
window.removeEventListener('mouseup', handleMouseUp);
130
};
131
}, [isScrubbing, duration]);
132
133
// Volume & Features
134
const toggleMute = (e: React.MouseEvent) => {
135
e.stopPropagation();
136
if (videoRef.current) {
137
videoRef.current.muted = !isMuted;
138
setIsMuted(!isMuted);
139
}
140
};
141
142
const toggleFullscreen = async (e: React.MouseEvent) => {
143
e.stopPropagation();
144
if (!containerRef.current) return;
145
146
if (!document.fullscreenElement) {
147
await containerRef.current.requestFullscreen();
148
setIsFullscreen(true);
149
} else {
150
await document.exitFullscreen();
151
setIsFullscreen(false);
152
}
153
};
154
155
const togglePiP = async (e: React.MouseEvent) => {
156
e.stopPropagation();
157
if (videoRef.current && document.pictureInPictureEnabled) {
158
if (document.pictureInPictureElement) {
159
await document.exitPictureInPicture();
160
} else {
161
await videoRef.current.requestPictureInPicture();
162
}
163
}
164
};
165
166
const cyclePlaybackRate = (e: React.MouseEvent) => {
167
e.stopPropagation();
168
if (videoRef.current) {
169
const newRate = playbackRate >= 2 ? 0.5 : playbackRate + 0.5;
170
videoRef.current.playbackRate = newRate;
171
setPlaybackRate(newRate);
172
}
173
};
174
175
const progressPercent = duration > 0 ? (currentTime / duration) * 100 : 0;
176
177
return (
178
<div
179
ref={containerRef}
180
className={cn(
181
"relative group bg-black overflow-hidden shadow-xl w-full",
182
isFullscreen ? "rounded-none" : "rounded-2xl",
183
className
184
)}
185
onMouseMove={handleMouseMove}
186
onMouseLeave={handleMouseLeave}
187
onClick={togglePlay}
188
>
189
<video
190
ref={videoRef}
191
src={src}
192
className="w-full h-full object-cover"
193
onTimeUpdate={handleTimeUpdate}
194
onLoadedMetadata={handleLoadedMetadata}
195
onEnded={() => setIsPlaying(false)}
196
onPlay={() => setIsPlaying(true)}
197
onPause={() => setIsPlaying(false)}
198
playsInline
199
/>
200
201
<AnimatePresence>
202
{(showControls || !isPlaying) && (
203
<motion.div
204
initial={{ opacity: 0 }}
205
animate={{ opacity: 1 }}
206
exit={{ opacity: 0 }}
207
transition={{ duration: 0.3 }}
208
className="absolute inset-0 bg-black/40 flex flex-col justify-between p-4 md:p-6"
209
>
210
{/* Top Bar */}
211
<div className="flex justify-between items-start z-10 w-full" onClick={e => e.stopPropagation()}>
212
<h2 className="text-white font-matter text-lg md:text-xl font-medium drop-shadow-md">
213
{title}
214
</h2>
215
{youtubeUrl && (
216
<a
217
href={youtubeUrl}
218
target="_blank"
219
rel="noopener noreferrer"
220
className="flex items-center gap-2 bg-black/20 hover:bg-black/40 backdrop-blur-md border border-white/20 text-white px-3 py-1.5 rounded-full text-xs font-matter font-medium transition-colors shadow-sm"
221
>
222
<Youtube className="w-4 h-4" />
223
<span>Watch on YouTube</span>
224
</a>
225
)}
226
</div>
227
228
{/* Center Controls */}
229
<div className="absolute inset-0 flex items-center justify-center gap-6 md:gap-8 pointer-events-none">
230
<button
231
onClick={(e) => skip(-10, e)}
232
className="pointer-events-auto flex items-center justify-center w-12 h-12 md:w-16 md:h-16 rounded-full bg-white/10 hover:bg-white/20 backdrop-blur-xl border border-white/20 text-white transition-all duration-300 hover:scale-105 active:scale-95"
233
>
234
<RotateCcw className="w-5 h-5 md:w-6 md:h-6" />
235
</button>
236
237
<button
238
onClick={togglePlay}
239
className="pointer-events-auto flex items-center justify-center w-16 h-16 md:w-20 md:h-20 rounded-full bg-white/20 hover:bg-white/30 backdrop-blur-xl border border-white/30 text-white transition-all duration-300 hover:scale-105 active:scale-95 shadow-[0_8px_32px_rgba(0,0,0,0.2)]"
240
>
241
{isPlaying ? (
242
<Pause className="w-8 h-8 md:w-10 md:h-10 fill-white" />
243
) : (
244
<Play className="w-8 h-8 md:w-10 md:h-10 fill-white ml-1.5" />
245
)}
246
</button>
247
248
<button
249
onClick={(e) => skip(10, e)}
250
className="pointer-events-auto flex items-center justify-center w-12 h-12 md:w-16 md:h-16 rounded-full bg-white/10 hover:bg-white/20 backdrop-blur-xl border border-white/20 text-white transition-all duration-300 hover:scale-105 active:scale-95"
251
>
252
<RotateCw className="w-5 h-5 md:w-6 md:h-6" />
253
</button>
254
</div>
255
256
{/* Bottom Controls */}
257
<div className="flex flex-col gap-3 z-10 w-full" onClick={e => e.stopPropagation()}>
258
{/* Progress Bar */}
259
<div
260
ref={progressRef}
261
onMouseDown={handleProgressMouseDown}
262
className="w-full h-1.5 md:h-2 bg-white/20 rounded-full cursor-pointer relative group flex items-center"
263
>
264
<div
265
className="h-full bg-white rounded-full relative"
266
style={{ width: \`\${progressPercent}%\` }}
267
>
268
<div className="absolute right-0 top-1/2 -translate-y-1/2 translate-x-1/2 w-3 h-3 md:w-4 md:h-4 bg-white rounded-full shadow-md opacity-0 group-hover:opacity-100 transition-opacity" />
269
</div>
270
</div>
271
272
{/* Bottom Toolbar */}
273
<div className="flex items-center justify-between text-white font-matter text-sm">
274
<div className="flex items-center gap-4">
275
<button onClick={togglePlay} className="hover:text-zinc-300 transition-colors">
276
{isPlaying ? <Pause className="w-5 h-5 fill-current" /> : <Play className="w-5 h-5 fill-current" />}
277
</button>
278
<div className="flex items-center gap-2 group/vol">
279
<button onClick={toggleMute} className="hover:text-zinc-300 transition-colors">
280
{isMuted || volume === 0 ? <VolumeX className="w-5 h-5" /> : <Volume2 className="w-5 h-5" />}
281
</button>
282
{/* Optional volume slider could go here */}
283
</div>
284
<div className="text-[13px] font-medium tracking-wide text-white/90 font-mono">
285
{formatTime(currentTime)} / {formatTime(duration)}
286
</div>
287
</div>
288
289
<div className="flex items-center gap-4">
290
<button
291
onClick={cyclePlaybackRate}
292
className="font-medium hover:text-zinc-300 transition-colors w-8 text-center"
293
>
294
{playbackRate}x
295
</button>
296
<button onClick={togglePiP} className="hover:text-zinc-300 transition-colors">
297
<PictureInPicture className="w-5 h-5" />
298
</button>
299
<button onClick={toggleFullscreen} className="hover:text-zinc-300 transition-colors">
300
{isFullscreen ? <Minimize className="w-5 h-5" /> : <Maximize className="w-5 h-5" />}
301
</button>
302
</div>
303
</div>
304
</div>
305
</motion.div>
306
)}
307
</AnimatePresence>
308
</div>
309
);
310
}
311