11'use client'
22
3- import { useRef , useEffect } from 'react'
3+ import { useRef , useEffect , useState , useCallback } from 'react'
44import { ImageData } from '../types'
55import { ChevronLeft , ChevronRight } from 'lucide-react'
66
@@ -13,6 +13,8 @@ interface ThumbnailRibbonProps {
1313export default function ThumbnailRibbon ( { images, selectedIndex, onSelect } : ThumbnailRibbonProps ) {
1414 const scrollContainerRef = useRef < HTMLDivElement > ( null )
1515 const thumbnailRefs = useRef < ( HTMLButtonElement | null ) [ ] > ( [ ] )
16+ const progressBarRef = useRef < HTMLDivElement > ( null )
17+ const [ isDragging , setIsDragging ] = useState ( false )
1618
1719 // Scroll to selected thumbnail when it changes
1820 useEffect ( ( ) => {
@@ -58,6 +60,47 @@ export default function ThumbnailRibbon({ images, selectedIndex, onSelect }: Thu
5860 }
5961 }
6062
63+ // Handle click/drag on progress bar to jump to position
64+ const handleProgressInteraction = useCallback ( ( clientX : number ) => {
65+ if ( ! progressBarRef . current || images . length === 0 ) return
66+
67+ const rect = progressBarRef . current . getBoundingClientRect ( )
68+ const relativeX = Math . max ( 0 , Math . min ( clientX - rect . left , rect . width ) )
69+ const percentage = relativeX / rect . width
70+ const newIndex = Math . min (
71+ Math . floor ( percentage * images . length ) ,
72+ images . length - 1
73+ )
74+ onSelect ( newIndex )
75+ } , [ images . length , onSelect ] )
76+
77+ const handleMouseDown = ( e : React . MouseEvent ) => {
78+ setIsDragging ( true )
79+ handleProgressInteraction ( e . clientX )
80+ }
81+
82+ const handleMouseMove = useCallback ( ( e : MouseEvent ) => {
83+ if ( isDragging ) {
84+ handleProgressInteraction ( e . clientX )
85+ }
86+ } , [ isDragging , handleProgressInteraction ] )
87+
88+ const handleMouseUp = useCallback ( ( ) => {
89+ setIsDragging ( false )
90+ } , [ ] )
91+
92+ // Add/remove mouse event listeners for drag
93+ useEffect ( ( ) => {
94+ if ( isDragging ) {
95+ window . addEventListener ( 'mousemove' , handleMouseMove )
96+ window . addEventListener ( 'mouseup' , handleMouseUp )
97+ }
98+ return ( ) => {
99+ window . removeEventListener ( 'mousemove' , handleMouseMove )
100+ window . removeEventListener ( 'mouseup' , handleMouseUp )
101+ }
102+ } , [ isDragging , handleMouseMove , handleMouseUp ] )
103+
61104 return (
62105 < div className = "bg-white/80 backdrop-blur-md rounded-xl border border-agi-teal/10 shadow-sm p-2 md:p-4" >
63106 < div className = "relative flex items-center" >
@@ -141,29 +184,48 @@ export default function ThumbnailRibbon({ images, selectedIndex, onSelect }: Thu
141184 </ button >
142185 </ div >
143186
144- { /* Progress indicator */ }
145- < div className = "mt-3 flex items-center justify-center gap-2" >
146- < div className = "flex gap-1" >
147- { ( ( ) => {
148- // Calculate group size to always show ~10 dots max
149- const maxDots = 10
150- const groupSize = Math . max ( 1 , Math . ceil ( images . length / maxDots ) )
151- const numDots = Math . ceil ( images . length / groupSize )
152- const currentGroup = Math . floor ( selectedIndex / groupSize )
153-
154- return Array . from ( { length : numDots } ) . map ( ( _ , i ) => (
187+ { /* Progress indicator - clickable/draggable slider */ }
188+ < div className = "mt-3 flex items-center justify-center gap-3" >
189+ < div
190+ ref = { progressBarRef }
191+ className = { `relative w-64 md:w-96 h-6 flex items-center cursor-pointer group ${ isDragging ? 'cursor-grabbing' : 'cursor-grab' } ` }
192+ onMouseDown = { handleMouseDown }
193+ role = "slider"
194+ aria-label = "Image position"
195+ aria-valuemin = { 1 }
196+ aria-valuemax = { images . length }
197+ aria-valuenow = { selectedIndex + 1 }
198+ tabIndex = { 0 }
199+ >
200+ { /* Track background */ }
201+ < div className = "absolute inset-x-0 h-2 bg-agi-teal/10 rounded-full" />
202+
203+ { /* Filled track */ }
204+ < div
205+ className = "absolute left-0 h-2 bg-gradient-to-r from-agi-teal to-agi-orange rounded-full transition-all duration-75"
206+ style = { { width : `${ ( ( selectedIndex + 1 ) / images . length ) * 100 } %` } }
207+ />
208+
209+ { /* Thumb/handle */ }
210+ < div
211+ className = { `absolute w-4 h-4 bg-white border-2 border-agi-teal rounded-full shadow-md transform -translate-x-1/2 transition-transform ${
212+ isDragging ? 'scale-125' : 'group-hover:scale-110'
213+ } `}
214+ style = { { left : `${ ( ( selectedIndex + 1 ) / images . length ) * 100 } %` } }
215+ />
216+
217+ { /* Segment markers */ }
218+ < div className = "absolute inset-x-0 flex justify-between px-0.5" >
219+ { Array . from ( { length : 11 } ) . map ( ( _ , i ) => (
155220 < div
156221 key = { i }
157- className = { `h-1 rounded-full transition-all ${
158- currentGroup === i
159- ? 'w-8 bg-gradient-to-r from-agi-teal to-agi-orange'
160- : 'w-2 bg-agi-teal/20'
161- } `}
222+ className = "w-0.5 h-1 bg-agi-teal/30 rounded-full"
162223 />
163- ) )
164- } ) ( ) }
224+ ) ) }
225+ </ div >
165226 </ div >
166- < span className = "text-xs text-agi-teal-600 ml-2" >
227+
228+ < span className = "text-xs text-agi-teal-600 font-medium min-w-[60px]" >
167229 { selectedIndex + 1 } / { images . length }
168230 </ span >
169231 </ div >
0 commit comments