@@ -48,6 +48,9 @@ export const LinkInput = ({ selectedLinks, onLinksChange, disabled = false }: Li
4848 const [ isAdding , setIsAdding ] = useState ( false )
4949 const [ inputValue , setInputValue ] = useState ( "" )
5050 const [ error , setError ] = useState < string | null > ( null )
51+ const [ draggedLinkId , setDraggedLinkId ] = useState < number | null > ( null )
52+ const [ dragOverIndex , setDragOverIndex ] = useState < number | null > ( null )
53+ const hasDraggedRef = useRef ( false )
5154 const inputRef = useRef < HTMLInputElement > ( null )
5255 const containerRef = useRef < HTMLDivElement > ( null )
5356
@@ -124,15 +127,84 @@ export const LinkInput = ({ selectedLinks, onLinksChange, disabled = false }: Li
124127 setError ( null )
125128 }
126129
127- const handleLinkClick = ( url : string ) => {
130+ const handleDragStart = ( e : React . DragEvent , linkId : number ) => {
131+ if ( disabled ) return
132+ setDraggedLinkId ( linkId )
133+ hasDraggedRef . current = false
134+ e . dataTransfer . effectAllowed = "move"
135+ e . dataTransfer . setData ( "text/html" , "" )
136+ }
137+
138+ const handleDragOver = ( e : React . DragEvent , index : number ) => {
139+ if ( disabled || draggedLinkId === null ) return
140+ e . preventDefault ( )
141+ e . dataTransfer . dropEffect = "move"
142+ setDragOverIndex ( index )
143+ hasDraggedRef . current = true
144+ }
145+
146+ const handleDragLeave = ( ) => {
147+ setDragOverIndex ( null )
148+ }
149+
150+ const handleDrop = ( e : React . DragEvent , dropIndex : number ) => {
151+ if ( disabled || draggedLinkId === null ) return
152+ e . preventDefault ( )
153+
154+ const draggedIndex = selectedLinks . findIndex ( ( l ) => l . id === draggedLinkId )
155+ if ( draggedIndex === - 1 || draggedIndex === dropIndex ) {
156+ setDraggedLinkId ( null )
157+ setDragOverIndex ( null )
158+ hasDraggedRef . current = false
159+ return
160+ }
161+
162+ const newLinks = [ ...selectedLinks ]
163+ const [ draggedLink ] = newLinks . splice ( draggedIndex , 1 )
164+ newLinks . splice ( dropIndex , 0 , draggedLink )
165+
166+ onLinksChange ( newLinks )
167+ setDraggedLinkId ( null )
168+ setDragOverIndex ( null )
169+ hasDraggedRef . current = false
170+ }
171+
172+ const handleDragEnd = ( ) => {
173+ setDraggedLinkId ( null )
174+ setDragOverIndex ( null )
175+ // Reset hasDragged after a short delay to allow click handler to check it
176+ setTimeout ( ( ) => {
177+ hasDraggedRef . current = false
178+ } , 0 )
179+ }
180+
181+ const handleLinkClick = ( url : string , e : React . MouseEvent ) => {
182+ // Prevent click if we just dragged
183+ if ( hasDraggedRef . current ) {
184+ e . preventDefault ( )
185+ e . stopPropagation ( )
186+ return
187+ }
128188 window . open ( url , "_blank" , "noopener,noreferrer" )
129189 }
130190
131191 return (
132192 < Container ref = { containerRef } >
133193 < LinkCloud >
134- { selectedLinks . map ( ( link ) => (
135- < LinkPill key = { link . id } onClick = { ( ) => handleLinkClick ( link . url ) } title = { link . url } >
194+ { selectedLinks . map ( ( link , index ) => (
195+ < LinkPill
196+ key = { link . id }
197+ $isDragging = { draggedLinkId === link . id }
198+ $dragOver = { dragOverIndex === index }
199+ draggable = { ! disabled }
200+ onDragStart = { ( e ) => handleDragStart ( e , link . id ) }
201+ onDragOver = { ( e ) => handleDragOver ( e , index ) }
202+ onDragLeave = { handleDragLeave }
203+ onDrop = { ( e ) => handleDrop ( e , index ) }
204+ onDragEnd = { handleDragEnd }
205+ onClick = { ( e ) => handleLinkClick ( link . url , e ) }
206+ title = { link . url }
207+ >
136208 { getDomainFromUrl ( link . url ) }
137209 { ! disabled && (
138210 < RemoveButton
@@ -188,7 +260,7 @@ const LinkCloud = styled.div`
188260 align-items: center;
189261`
190262
191- const LinkPill = styled . div `
263+ const LinkPill = styled . div < { $isDragging ?: boolean ; $dragOver ?: boolean } > `
192264 display: inline-flex;
193265 align-items: center;
194266 gap: 0.375rem;
@@ -200,11 +272,19 @@ const LinkPill = styled.div`
200272 color: white;
201273 font-weight: 500;
202274 transition: all 0.2s ease;
203- cursor: pointer;
275+ cursor: ${ ( props ) => ( props . draggable ? "grab" : "pointer" ) } ;
276+ opacity: ${ ( props ) => ( props . $isDragging ? 0.5 : 1 ) } ;
277+ transform: ${ ( props ) => ( props . $isDragging ? "scale(0.95)" : "scale(1)" ) } ;
278+ border-color: ${ ( props ) =>
279+ props . $dragOver ? "rgba(255, 255, 255, 0.6)" : "rgba(255, 255, 255, 0.3)" } ;
204280
205281 &:hover {
206282 background-color: rgba(255, 255, 255, 0.25);
207283 }
284+
285+ &:active {
286+ cursor: ${ ( props ) => ( props . draggable ? "grabbing" : "pointer" ) } ;
287+ }
208288`
209289
210290const RemoveButton = styled . button `
0 commit comments