1+ import React , { useEffect , useRef , useState } from "react" ;
2+ import { motion , AnimatePresence } from "framer-motion" ;
3+
4+ export default function LinkedListVisualizer ( {
5+ array = [ ] ,
6+ highlight = null ,
7+ variant = "singly" ,
8+ } ) {
9+ const containerRef = useRef ( null ) ;
10+ const [ positions , setPositions ] = useState ( [ ] ) ;
11+ const [ containerBounds , setContainerBounds ] = useState ( null ) ;
12+
13+ // Recalculate node positions when array changes
14+ useEffect ( ( ) => {
15+ if ( ! containerRef . current || array . length === 0 ) {
16+ setPositions ( [ ] ) ;
17+ return ;
18+ }
19+
20+ // Wait briefly for DOM to update
21+ const timer = setTimeout ( ( ) => {
22+ const container = containerRef . current ?. getBoundingClientRect ( ) ;
23+ const nodes = Array . from (
24+ containerRef . current . querySelectorAll ( ".ll-node" )
25+ ) ;
26+
27+ if ( nodes . length > 0 && container ) {
28+ const rects = nodes . map ( ( n ) => n . getBoundingClientRect ( ) ) ;
29+ setPositions ( rects ) ;
30+ setContainerBounds ( container ) ;
31+ }
32+ } , 60 ) ;
33+
34+ return ( ) => clearTimeout ( timer ) ;
35+ } , [ array . length ] ) ; // triggers on insert/delete
36+
37+ if ( ! array || array . length === 0 )
38+ return (
39+ < div className = "flex items-center justify-center text-slate-400 text-lg h-40" >
40+ List is empty
41+ </ div >
42+ ) ;
43+
44+ const isCircular = variant === "circular" ;
45+ const isDoubly = variant === "doubly" ;
46+
47+ // Color logic for different highlight actions
48+ const getNodeColor = ( idx ) => {
49+ const isActive = highlight && highlight . index === idx ;
50+ const type = highlight ?. type ;
51+ if ( type === "insert" && isActive ) return "bg-emerald-700 text-white" ;
52+ if ( type === "delete" && isActive ) return "bg-rose-600 text-white" ;
53+ if ( type === "traverse" && isActive ) return "bg-yellow-500 text-black" ;
54+ return "bg-slate-800 text-white" ;
55+ } ;
56+
57+ // ✅ Improved circular path calculation (perfectly connects)
58+ const getCircularPath = ( ) => {
59+ if ( ! isCircular || positions . length < 2 || ! containerBounds ) return null ;
60+
61+ const first = positions [ 0 ] ;
62+ const last = positions [ positions . length - 1 ] ;
63+
64+ // Calculate path start/end near bottom centers
65+ const startX =
66+ last . right - containerBounds . left - last . width * 0.2 ; // tail bottom-right inward
67+ const startY = last . bottom - containerBounds . top - 5 ;
68+
69+ const endX = first . left - containerBounds . left + first . width * 0.2 ; // head bottom-left inward
70+ const endY = first . bottom - containerBounds . top - 5 ;
71+
72+ // Control point for smooth curve below nodes
73+ const controlY = Math . max ( startY , endY ) + 60 ;
74+ const midX = ( startX + endX ) / 2 ;
75+
76+ return {
77+ path : `M ${ startX } ${ startY } Q ${ midX } ${ controlY } ${ endX } ${ endY } ` ,
78+ startX,
79+ startY,
80+ endX,
81+ endY,
82+ } ;
83+ } ;
84+
85+ const circularPath = getCircularPath ( ) ;
86+
87+ return (
88+ < div
89+ ref = { containerRef }
90+ className = "relative flex items-center justify-center gap-6 p-6 min-h-[240px]"
91+ >
92+ { /* --- SVG Circular Connection --- */ }
93+ { isCircular && circularPath && (
94+ < svg
95+ className = "absolute inset-0 w-full h-full pointer-events-none"
96+ xmlns = "http://www.w3.org/2000/svg"
97+ >
98+ < defs >
99+ < linearGradient id = "circular-gradient" x1 = "0%" y1 = "0%" x2 = "100%" y2 = "0%" >
100+ < stop offset = "0%" stopColor = "#22c55e" />
101+ < stop offset = "100%" stopColor = "#0ea5e9" />
102+ </ linearGradient >
103+ </ defs >
104+
105+ < motion . path
106+ d = { circularPath . path }
107+ stroke = "url(#circular-gradient)"
108+ strokeWidth = "2.5"
109+ fill = "none"
110+ strokeDasharray = "8 5"
111+ initial = { { pathLength : 0 } }
112+ animate = { { pathLength : 1 } }
113+ transition = { { duration : 1.2 , ease : "easeInOut" } }
114+ />
115+
116+ { /* Label on the path */ }
117+ < text
118+ x = { ( circularPath . startX + circularPath . endX ) / 2 }
119+ y = { Math . max ( circularPath . startY , circularPath . endY ) + 50 }
120+ textAnchor = "middle"
121+ className = "fill-slate-300 text-xs"
122+ >
123+ Tail → Head
124+ </ text >
125+ </ svg >
126+ ) }
127+
128+ { /* --- Nodes --- */ }
129+ < AnimatePresence mode = "popLayout" >
130+ { array . map ( ( val , idx ) => (
131+ < motion . div
132+ key = { `${ val } -${ idx } ` }
133+ layout
134+ initial = { { scale : 0.8 , opacity : 0 } }
135+ animate = { { scale : 1 , opacity : 1 } }
136+ exit = { { scale : 0.6 , opacity : 0 } }
137+ transition = { { duration : 0.3 } }
138+ className = { `relative ll-node flex flex-col items-center justify-center w-16 h-16 rounded-lg shadow-md ${ getNodeColor (
139+ idx
140+ ) } `}
141+ >
142+ { /* Index label */ }
143+ < div className = "absolute -top-5 text-xs text-slate-400" >
144+ [{ idx } ]
145+ </ div >
146+
147+ { /* Node value */ }
148+ < div className = "text-base font-semibold" > { val } </ div >
149+
150+ { /* HEAD label */ }
151+ { idx === 0 && (
152+ < span className = "absolute -bottom-6 text-xs text-emerald-400 font-semibold" >
153+ HEAD
154+ </ span >
155+ ) }
156+
157+ { /* TAIL label */ }
158+ { idx === array . length - 1 && (
159+ < span
160+ className = { `absolute text-xs font-semibold ${
161+ isCircular
162+ ? "top-[70px] text-cyan-400"
163+ : "-bottom-6 text-rose-400"
164+ } `}
165+ >
166+ TAIL
167+ </ span >
168+ ) }
169+
170+ { /* Doubly linked backward pointer */ }
171+ { isDoubly && idx > 0 && (
172+ < span className = "absolute -left-5 text-lg text-slate-500" > ←</ span >
173+ ) }
174+
175+ { /* Forward pointer */ }
176+ { ( idx < array . length - 1 || isCircular ) && (
177+ < span className = "absolute -right-5 text-lg text-slate-500" > →</ span >
178+ ) }
179+ </ motion . div >
180+ ) ) }
181+ </ AnimatePresence >
182+
183+ { /* --- Info Label --- */ }
184+ { isCircular && (
185+ < div className = "absolute bottom-2 text-slate-400 text-sm italic" >
186+ ↻ Circular Linked List — Tail connects back to Head
187+ </ div >
188+ ) }
189+ </ div >
190+ ) ;
191+ }
192+
0 commit comments