Introducing Nuvyx UI v1.0.0

Animated Code Block

A code display component with real-time typing effects. Supports syntax highlighting and customizable themes.

Animated Code Block

Select a theme to customize the code display

Select Theme

javascript
1
2
3
4
5
6
7
8
9
10
11
Active theme: dark

Installation Guide

1

Install Dependencies

Framer Motion

npm install framer-motion

Lucide React

npm install lucide-react

Utility Functions

npm install clsx tailwind-merge
2

Setup Configuration

Create file: /lib/utils.ts
/lib/utils.ts
Configuration
1import { clsx, type ClassValue } from "clsx";
2import { twMerge } from "tailwind-merge";
3
4export function cn(...inputs: ClassValue[]) {
5  return twMerge(clsx(inputs));
6}
3

Copy Component Code

Animated Code Block.tsx
TypeScript
1"use client";
2
3import { useState, useEffect, useRef } from "react";
4import { motion } from "framer-motion";
5import { cn } from "@/lib/utils";
6import { Play, Pause, Copy, Check, Terminal, RotateCcw } from "lucide-react";
7
8export interface AnimatedCodeBlockProps {
9  code: string;
10  language?: string;
11  theme?: "dark" | "light" | "terminal" | "cyberpunk" | "minimal" | "nuvyx";
12  typingSpeed?: number;
13  showLineNumbers?: boolean;
14  highlightLines?: number[];
15  title?: string;
16  className?: string;
17  autoPlay?: boolean;
18  loop?: boolean;
19  blurEffect?: boolean;
20  showControls?: boolean;
21  onCopy?: () => void;
22}
23
24export function AnimatedCodeBlock({
25  code,
26  language = "javascript",
27  theme = "dark",
28  typingSpeed = 50,
29  showLineNumbers = true,
30  highlightLines = [],
31  title,
32  className,
33  autoPlay = false,
34  loop = false,
35  blurEffect = false,
36  showControls = true,
37  onCopy,
38}: AnimatedCodeBlockProps) {
39  const [isPlaying, setIsPlaying] = useState(autoPlay);
40  const [currentPosition, setCurrentPosition] = useState(0);
41  const [copied, setCopied] = useState(false);
42  const [completed, setCompleted] = useState(false);
43  const [isPaused, setIsPaused] = useState(false);
44  const codeRef = useRef<HTMLDivElement>(null);
45  const timerRef = useRef<NodeJS.Timeout | null>(null);
46
47  const getThemeStyles = () => {
48    switch (theme) {
49      case "dark":
50        return {
51          background: "bg-gray-900",
52          text: "text-gray-100",
53          lineNumbers: "text-gray-500",
54          highlight: "bg-gray-800",
55          border: "border-gray-700",
56        };
57      case "minimal":
58        return {
59          background: "bg-yellow-100",
60          text: "text-gray-900",
61          lineNumbers: "text-gray-500",
62          highlight: "bg-yellow-200",
63          border: "border-yellow-300",
64        };
65      case "terminal":
66        return {
67          background: "bg-black",
68          text: "text-green-400",
69          lineNumbers: "text-green-700",
70          highlight: "bg-green-900/30",
71          border: "border-green-900",
72        };
73      case "cyberpunk":
74        return {
75          background: "bg-purple-950",
76          text: "text-pink-300",
77          lineNumbers: "text-purple-600",
78          highlight: "bg-pink-900/30",
79          border: "border-pink-700",
80        };
81      case "light":
82        return {
83          background: "bg-white",
84          text: "text-gray-800",
85          lineNumbers: "text-gray-400",
86          highlight: "bg-gray-200",
87          border: "border-gray-200",
88        };
89      case "nuvyx":
90        return {
91          background: "bg-black",
92          text: "text-purple-100",
93          lineNumbers: "text-purple-500",
94          highlight: "bg-purple-900/30",
95          border: "border-purple-700",
96        };
97      default:
98        return {
99          background: "bg-gray-900",
100          text: "text-gray-100",
101          lineNumbers: "text-gray-500",
102          highlight: "bg-gray-800",
103          border: "border-gray-700",
104        };
105    }
106  };
107
108  const themeStyles = getThemeStyles();
109
110  useEffect(() => {
111    if (isPlaying && currentPosition < code.length) {
112      timerRef.current = setTimeout(() => {
113        setCurrentPosition(currentPosition + 1);
114      }, typingSpeed);
115    } else if (isPlaying && currentPosition >= code.length) {
116      if (loop) {
117        setTimeout(() => {
118          setCurrentPosition(0);
119        }, 1000);
120      } else {
121        setIsPlaying(false);
122        setCompleted(true);
123        setIsPaused(false);
124      }
125    }
126
127    return () => {
128      if (timerRef.current) clearTimeout(timerRef.current);
129    };
130  }, [isPlaying, currentPosition, code, typingSpeed, loop]);
131
132  const togglePlay = () => {
133    if (isPlaying) {
134      setIsPaused(true);
135    } else if (completed) {
136      restartAnimation();
137    } else {
138      setIsPaused(false);
139    }
140    setIsPlaying(!isPlaying);
141  };
142
143  const restartAnimation = () => {
144    setCurrentPosition(0);
145    setIsPlaying(true);
146    setCompleted(false);
147    setIsPaused(false);
148  };
149
150  const copyCode = () => {
151    navigator.clipboard.writeText(code);
152    setCopied(true);
153    setTimeout(() => setCopied(false), 2000);
154    if (onCopy) onCopy();
155  };
156
157  const codeLines = code.split("\n");
158  const renderLines = () => {
159    let remainingChars = currentPosition;
160    const result = [];
161
162    for (let i = 0; i < codeLines.length; i++) {
163      const line = codeLines[i];
164      const lineLength = line.length + 1;
165
166      if (remainingChars <= 0) {
167        result.push("");
168      } else if (remainingChars >= lineLength) {
169        result.push(line);
170        remainingChars -= lineLength;
171      } else {
172        result.push(line.substring(0, remainingChars));
173        remainingChars = 0;
174      }
175    }
176
177    return result;
178  };
179
180  const displayedLines = completed ? code.split("\n") : renderLines();
181
182  const getCursorLineIndex = () => {
183    if (!isPlaying && !isPaused) return -1;
184
185    let charsProcessed = 0;
186    for (let i = 0; i < codeLines.length; i++) {
187      const lineLength = codeLines[i].length + 1;
188      charsProcessed += lineLength;
189
190      if (currentPosition < charsProcessed) {
191        return i;
192      }
193    }
194
195    return codeLines.length - 1;
196  };
197
198  const cursorLineIndex = getCursorLineIndex();
199
200  return (
201    <div
202      className={cn(
203        "animated-code-block rounded-lg overflow-hidden",
204        themeStyles.background,
205        themeStyles.text,
206        themeStyles.border,
207        "border",
208        className
209      )}
210    >
211      <div className="flex items-center justify-between p-2 border-b border-opacity-20">
212        <div className="flex items-center gap-2">
213          <Terminal size={16} />
214          <span className="text-sm font-medium">{title || language}</span>
215        </div>
216        {showControls && (
217          <div className="flex items-center gap-2">
218            {completed ? (
219              <button
220                onClick={restartAnimation}
221                className="p-1 rounded hover:bg-gray-700 hover:bg-opacity-30 transition-colors"
222                aria-label="Repeat animation"
223              >
224                <RotateCcw size={14} />
225              </button>
226            ) : (
227              <button
228                onClick={togglePlay}
229                className="p-1 rounded hover:bg-gray-700 hover:bg-opacity-30 transition-colors"
230                aria-label={isPlaying ? "Pause" : "Play"}
231              >
232                {isPlaying ? <Pause size={14} /> : <Play size={14} />}
233              </button>
234            )}
235            <button
236              onClick={copyCode}
237              className="p-1 rounded hover:bg-gray-700 hover:bg-opacity-30 transition-colors"
238              aria-label="Copy code"
239            >
240              {copied ? <Check size={14} /> : <Copy size={14} />}
241            </button>
242          </div>
243        )}
244      </div>
245
246      <div className="relative overflow-hidden">
247        {blurEffect && theme === "terminal" && (
248          <div className="absolute inset-0 pointer-events-none bg-green-400 opacity-[0.03] mix-blend-overlay" />
249        )}
250
251        <div className="overflow-x-auto">
252          <div className="flex min-w-full">
253            {showLineNumbers && (
254              <div
255                className={cn(
256                  "text-xs py-4 px-2 text-right select-none",
257                  themeStyles.lineNumbers
258                )}
259              >
260                {codeLines.map((_, i) => (
261                  <div key={i} className="h-6 flex items-center justify-end">
262                    {i + 1}
263                  </div>
264                ))}
265              </div>
266            )}
267
268            <div className="relative py-4 flex-grow" ref={codeRef}>
269              {highlightLines.map((lineNum) => (
270                <div
271                  key={`highlight-${lineNum}`}
272                  className={cn(
273                    "absolute left-0 right-0 h-6",
274                    themeStyles.highlight
275                  )}
276                  style={{ top: `${(lineNum - 1) * 24 + 16}px` }}
277                />
278              ))}
279
280              <div className="relative z-10 px-4 font-mono text-sm">
281                {codeLines.map((line, i) => (
282                  <div key={i} className="h-6 whitespace-pre">
283                    {displayedLines[i] || ""}
284                    {i === cursorLineIndex && (
285                      <motion.span
286                        className="inline-block w-2 h-5 bg-current -mb-0.5"
287                        animate={{ opacity: [1, 0] }}
288                        transition={{
289                          repeat: Number.POSITIVE_INFINITY,
290                          duration: 0.8,
291                        }}
292                      />
293                    )}
294                  </div>
295                ))}
296              </div>
297            </div>
298          </div>
299        </div>
300      </div>
301    </div>
302  );
303}
304
4

Final Steps

Update Import Paths

Make sure to update the import paths in the component code to match your project structure. For example, change @/components/ui/button to match your UI components location.

Props

NameTypeDefaultDescription
codestring""The code to display and animate
languagestring"javascript"The programming language for syntax highlighting
themestringdarkThe visual theme of the code block (dark, light, terminal, cyberpunk, minimal, nuvyx)
typingSpeednumber50The speed of the typing animation in milliseconds
showLineNumbersbooleantrueWhether to show line numbers
highlightLinesnumber[][]Array of line numbers to highlight
titlestring""Title to display in the header
autoPlaybooleanfalseWhether to start the animation automatically
loopbooleanfalseWhether to loop the animation
blurEffectbooleanfalseWhether to add a terminal blur effect
showControlsbooleantrueWhether to show play/pause and copy controls
onCopy() => voidundefinedCallback function when code is copied
classNamestring""Additional CSS classes to apply

Examples

Docker Multi-Stage Build
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Advanced Button Animation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17