11import type { Span } from "dnd-timeline" ;
2- import { useItem } from "dnd-timeline" ;
2+ import { useItem , useTimelineContext } from "dnd-timeline" ;
33import { Gauge , MessageSquare , Scissors , ZoomIn } from "lucide-react" ;
44import { useMemo } from "react" ;
55import { cn } from "@/lib/utils" ;
6+ import {
7+ DEFAULT_ZOOM_IN_MS ,
8+ DEFAULT_ZOOM_OUT_MS ,
9+ getDurations ,
10+ } from "../videoPlayback/zoomRegionUtils" ;
611import glassStyles from "./ItemGlass.module.css" ;
712
813interface ItemProps {
@@ -13,7 +18,10 @@ interface ItemProps {
1318 isSelected ?: boolean ;
1419 onSelect ?: ( ) => void ;
1520 zoomDepth ?: number ;
21+ zoomInDurationMs ?: number ;
22+ zoomOutDurationMs ?: number ;
1623 speedValue ?: number ;
24+ onZoomDurationChange ?: ( id : string , zoomIn : number , zoomOut : number ) => void ;
1725 variant ?: "zoom" | "trim" | "annotation" | "speed" | "blur" ;
1826}
1927
@@ -44,10 +52,14 @@ export default function Item({
4452 isSelected = false ,
4553 onSelect,
4654 zoomDepth = 1 ,
55+ zoomInDurationMs,
56+ zoomOutDurationMs,
4757 speedValue,
4858 variant = "zoom" ,
4959 children,
60+ onZoomDurationChange,
5061} : ItemProps ) {
62+ const { pixelsToValue } = useTimelineContext ( ) ;
5163 const { setNodeRef, attributes, listeners, itemStyle, itemContentStyle } = useItem ( {
5264 id,
5365 span,
@@ -79,6 +91,16 @@ export default function Item({
7991 const MIN_ITEM_PX = 6 ;
8092 const safeItemStyle = { ...itemStyle , minWidth : MIN_ITEM_PX } ;
8193
94+ const { zoomIn, zoomOut } = useMemo ( ( ) => {
95+ if ( ! isZoom ) return { zoomIn : 0 , zoomOut : 0 } ;
96+ return getDurations ( {
97+ startMs : span . start ,
98+ endMs : span . end ,
99+ zoomInDurationMs,
100+ zoomOutDurationMs,
101+ } ) ;
102+ } , [ isZoom , span . start , span . end , zoomInDurationMs , zoomOutDurationMs ] ) ;
103+
82104 return (
83105 < div
84106 ref = { setNodeRef }
@@ -101,6 +123,98 @@ export default function Item({
101123 onSelect ?.( ) ;
102124 } }
103125 >
126+ { isZoom && (
127+ < >
128+ { /* Transition In Marker */ }
129+ < div
130+ className = "absolute top-0 bottom-0 left-0 bg-white/10 border-r border-white/20 pointer-events-none"
131+ style = { {
132+ width : `${ ( zoomIn / ( span . end - span . start ) ) * 100 } %` ,
133+ } }
134+ />
135+ { /* Draggable handle for Transition In */ }
136+ < div
137+ className = "absolute top-0 bottom-0 w-2 cursor-col-resize z-20 group-hover:bg-white/5 transition-colors"
138+ style = { {
139+ left : `${ ( zoomIn / ( span . end - span . start ) ) * 100 } %` ,
140+ transform : "translateX(-50%)" ,
141+ } }
142+ onPointerDown = { ( e ) => {
143+ e . stopPropagation ( ) ;
144+ e . preventDefault ( ) ;
145+ const target = e . currentTarget ;
146+ target . setPointerCapture ( e . pointerId ) ;
147+
148+ const startX = e . clientX ;
149+ const initialZoomIn = zoomInDurationMs ?? DEFAULT_ZOOM_IN_MS ;
150+ const initialZoomOut = zoomOutDurationMs ?? DEFAULT_ZOOM_OUT_MS ;
151+
152+ const onPointerMove = ( moveEvent : PointerEvent ) => {
153+ const deltaPx = moveEvent . clientX - startX ;
154+ const deltaMs = pixelsToValue ( deltaPx ) ;
155+ const newDuration = Math . max (
156+ 0 ,
157+ Math . min ( initialZoomIn + deltaMs , span . end - span . start - initialZoomOut ) ,
158+ ) ;
159+ onZoomDurationChange ?.( id , newDuration , initialZoomOut ) ;
160+ } ;
161+
162+ const onPointerUp = ( ) => {
163+ target . releasePointerCapture ( e . pointerId ) ;
164+ window . removeEventListener ( "pointermove" , onPointerMove ) ;
165+ window . removeEventListener ( "pointerup" , onPointerUp ) ;
166+ } ;
167+
168+ window . addEventListener ( "pointermove" , onPointerMove ) ;
169+ window . addEventListener ( "pointerup" , onPointerUp ) ;
170+ } }
171+ />
172+ { /* Transition Out Marker */ }
173+ < div
174+ className = "absolute top-0 bottom-0 right-0 bg-white/10 border-l border-white/20 pointer-events-none"
175+ style = { {
176+ width : `${ ( zoomOut / ( span . end - span . start ) ) * 100 } %` ,
177+ } }
178+ />
179+ { /* Draggable handle for Transition Out */ }
180+ < div
181+ className = "absolute top-0 bottom-0 w-2 cursor-col-resize z-20 group-hover:bg-white/5 transition-colors"
182+ style = { {
183+ right : `${ ( zoomOut / ( span . end - span . start ) ) * 100 } %` ,
184+ transform : "translateX(50%)" ,
185+ } }
186+ onPointerDown = { ( e ) => {
187+ e . stopPropagation ( ) ;
188+ e . preventDefault ( ) ;
189+ const target = e . currentTarget ;
190+ target . setPointerCapture ( e . pointerId ) ;
191+
192+ const startX = e . clientX ;
193+ const initialZoomIn = zoomInDurationMs ?? DEFAULT_ZOOM_IN_MS ;
194+ const initialZoomOut = zoomOutDurationMs ?? DEFAULT_ZOOM_OUT_MS ;
195+
196+ const onPointerMove = ( moveEvent : PointerEvent ) => {
197+ const deltaPx = startX - moveEvent . clientX ; // Inverted because right-anchored
198+ const deltaMs = pixelsToValue ( deltaPx ) ;
199+ const newDuration = Math . max (
200+ 0 ,
201+ Math . min ( initialZoomOut + deltaMs , span . end - span . start - initialZoomIn ) ,
202+ ) ;
203+ onZoomDurationChange ?.( id , initialZoomIn , newDuration ) ;
204+ } ;
205+
206+ const onPointerUp = ( ) => {
207+ target . releasePointerCapture ( e . pointerId ) ;
208+ window . removeEventListener ( "pointermove" , onPointerMove ) ;
209+ window . removeEventListener ( "pointerup" , onPointerUp ) ;
210+ } ;
211+
212+ window . addEventListener ( "pointermove" , onPointerMove ) ;
213+ window . addEventListener ( "pointerup" , onPointerUp ) ;
214+ } }
215+ />
216+ </ >
217+ ) }
104218 < div
105219 className = { cn ( glassStyles . zoomEndCap , glassStyles . left ) }
106220 style = { {
0 commit comments