import React, { useState, useRef, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { createPortal } from 'react-dom'; import { X, GripHorizontal, Maximize2, Minimize2 } from 'lucide-react'; import { Button } from '../ui/button'; import { cn } from '../shared/utils'; interface FloatingMediaWrapperProps { children: React.ReactNode; isFloating: boolean; setIsFloating: (floating: boolean) => void; title?: string; onClose?: () => void; onCloseMedia?: () => void; aspectRatio?: number; className?: string; minWidth?: number; minHeight?: number; colorVariant?: 'default' | 'primary'; playerId?: string; enablePadding?: boolean; } export function FloatingMediaWrapper({ children, isFloating, setIsFloating, title, onClose, onCloseMedia, aspectRatio = 16 / 9, className, minWidth = 320, minHeight = 110, colorVariant = 'default', playerId = 'default', enablePadding = false, }: FloatingMediaWrapperProps) { const { t } = useTranslation(); const [isMounted, setIsMounted] = useState(false); useEffect(() => { setIsMounted(true); return () => setIsMounted(false); }, []); // Placeholder when content is floating if (isFloating) { return ( <>

{t('media.playingFloating')}

{isMounted && createPortal( {children} , document.body )} ); } return (
{children}
); } interface FloatingContainerProps { children: React.ReactNode; setIsFloating: (floating: boolean) => void; onClose?: () => void; onCloseMedia?: () => void; title?: string; aspectRatio: number; minWidth: number; minHeight: number; colorVariant: 'default' | 'primary'; playerId?: string; enablePadding?: boolean; } function FloatingContainer({ children, setIsFloating, onClose, onCloseMedia, title, aspectRatio, minWidth, minHeight, colorVariant, playerId = 'default', enablePadding = false, }: FloatingContainerProps) { const { t } = useTranslation(); const minPlayerHeight = minWidth / aspectRatio; const containerRef = useRef(null); const resizeHandleRef = useRef(null); const getInitialState = useCallback(() => { const storageKey = `xertica-media-player-${playerId}`; const stored = typeof window !== 'undefined' ? window.localStorage?.getItem(storageKey) : null; if (stored) { try { return JSON.parse(stored); } catch { // invalid stored data } } return { position: { x: typeof window !== 'undefined' ? window.innerWidth - minWidth - 24 : 0, y: typeof window !== 'undefined' ? window.innerHeight - minPlayerHeight - 24 : 0, }, size: { width: minWidth, height: minPlayerHeight, }, }; }, [minWidth, minPlayerHeight, playerId]); interface FloatingState { position: { x: number; y: number }; size: { width: number; height: number }; } const [state, setState] = useState(getInitialState); const { position, size } = state; const [isDragging, setIsDragging] = useState(false); const [isResizing, setIsResizing] = useState(false); const dragStartPos = useRef({ x: 0, y: 0 }); const dragStartOffset = useRef({ x: 0, y: 0 }); const resizeStartState = useRef({ position: { x: 0, y: 0 }, size: { width: 0, height: 0 } }); const saveState = useCallback( (newPosition: { x: number; y: number }, newSize: { width: number; height: number }) => { if (typeof window === 'undefined') return; const storageKey = `xertica-media-player-${playerId}`; window.localStorage?.setItem( storageKey, JSON.stringify({ position: newPosition, size: newSize }) ); }, [playerId] ); const handleMouseDown = (e: React.MouseEvent) => { setIsDragging(true); dragStartPos.current = { x: e.clientX, y: e.clientY }; dragStartOffset.current = { x: position.x, y: position.y }; }; const handleResizeStart = (e: React.MouseEvent, direction: string) => { e.stopPropagation(); setIsResizing(true); resizeHandleRef.current = direction; dragStartPos.current = { x: e.clientX, y: e.clientY }; resizeStartState.current = { position: { ...position }, size: { ...size } }; }; const handleMouseMove = useCallback( (e: MouseEvent) => { if (isDragging) { const deltaX = e.clientX - dragStartPos.current.x; const deltaY = e.clientY - dragStartPos.current.y; const newPos = { x: dragStartOffset.current.x + deltaX, y: dragStartOffset.current.y + deltaY, }; setState(prev => ({ ...prev, position: newPos })); saveState(newPos, state.size); } if (isResizing && resizeHandleRef.current) { const deltaX = e.clientX - dragStartPos.current.x; const deltaY = e.clientY - dragStartPos.current.y; const direction = resizeHandleRef.current; let newPos = { ...resizeStartState.current.position }; let newSize = { ...resizeStartState.current.size }; // Right resize - only increase width if (direction.includes('right')) { newSize.width = Math.max(minWidth, resizeStartState.current.size.width + deltaX); } // Left resize - adjust width and position if (direction.includes('left')) { const newW = Math.max(minWidth, resizeStartState.current.size.width - deltaX); newPos.x = resizeStartState.current.position.x + (resizeStartState.current.size.width - newW); newSize.width = newW; } // Bottom resize - only increase height if (direction.includes('bottom')) { newSize.height = Math.max(minHeight, resizeStartState.current.size.height + deltaY); } // Top resize - adjust height and position if (direction.includes('top')) { const newH = Math.max(minHeight, resizeStartState.current.size.height - deltaY); newPos.y = resizeStartState.current.position.y + (resizeStartState.current.size.height - newH); newSize.height = newH; } setState({ position: newPos, size: newSize }); saveState(newPos, newSize); } }, [isDragging, isResizing, state.size, minWidth, minHeight, saveState] ); const handleMouseUp = useCallback(() => { setIsDragging(false); setIsResizing(false); resizeHandleRef.current = null; }, []); useEffect(() => { if (!isDragging && !isResizing) return; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); return () => { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; }, [isDragging, isResizing, handleMouseMove, handleMouseUp]); return (
{title || t('media.playingMedia')}
{children}
{/* Resize handles — mouse-only, hidden from assistive tech */}