diff --git a/messages/en.json b/messages/en.json
index 52de43e..d6dc946 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -70,5 +70,80 @@
"current": "Current Element",
"sorted": "Sorted",
"length": "Length:"
+ },
+ "algorithm-explanation": {
+ "stats": {
+ "title": "Statistics",
+ "iterations": "Iterations",
+ "swaps": "Swaps"
+ },
+ "complexity": {
+ "title": "Complexity Analysis",
+ "case": "Case",
+ "best": "Best",
+ "average": "Average",
+ "worst": "Worst",
+ "time": "Time",
+ "space": "Space"
+ },
+ "explanation": {
+ "title": "How It Works",
+ "steps": "Steps",
+ "cases": "Cases",
+ "code": "Code"
+ },
+ "bubble": {
+ "title": "Bubble Sort",
+ "description": "A simple comparison-based algorithm that repeatedly steps through the list, compares adjacent elements, and swaps them if they are in the wrong order.",
+ "steps": [
+ "Start at the beginning of the array",
+ "Compare adjacent elements. If the first is greater than the second, swap them",
+ "Move to the next pair of elements and repeat the comparison and swap if necessary",
+ "After one complete pass, the largest element will be at the end. Repeat the process for the remaining elements"
+ ],
+ "best-case": "When the array is already sorted, bubble sort makes only one pass through the array with no swaps.",
+ "average-case": "For random arrays, bubble sort still requires quadratic time.",
+ "worst-case": "When the array is sorted in reverse order, bubble sort must make the maximum number of comparisons and swaps."
+ },
+ "selection": {
+ "title": "Selection Sort",
+ "description": "A simple in-place comparison sorting algorithm that divides the input list into two parts: a sorted sublist and an unsorted sublist.",
+ "steps": [
+ "Find the minimum element in the unsorted part of the array",
+ "Swap it with the element at the beginning of the unsorted part",
+ "Move the boundary between the sorted and unsorted parts one element to the right",
+ "Repeat until the entire array is sorted"
+ ],
+ "best-case": "Selection sort always performs the same number of comparisons regardless of the input array's order.",
+ "average-case": "For random arrays, selection sort requires quadratic time.",
+ "worst-case": "Selection sort always performs the same number of comparisons and swaps regardless of the input array's order."
+ },
+ "insertion": {
+ "title": "Insertion Sort",
+ "description": "A simple sorting algorithm that builds the final sorted array one item at a time, similar to how you might sort playing cards in your hand.",
+ "steps": [
+ "Start with the second element (assume the first element is already sorted)",
+ "Compare the current element with the previous elements",
+ "If the previous element is greater than the current element, move the previous element one position ahead",
+ "Repeat until the correct position for the current element is found, then insert it"
+ ],
+ "best-case": "When the array is already sorted, insertion sort makes only one comparison per element.",
+ "average-case": "For random arrays, insertion sort requires quadratic time.",
+ "worst-case": "When the array is sorted in reverse order, insertion sort must shift each element to the beginning of the array."
+ },
+ "quicksort": {
+ "title": "Quick Sort",
+ "description": "An efficient, divide-and-conquer sorting algorithm that works by selecting a 'pivot' element and partitioning the array around the pivot.",
+ "steps": [
+ "Choose a pivot element from the array",
+ "Partition the array: items less than the pivot go to the left, items greater go to the right",
+ "The pivot is now in its final sorted position",
+ "Recursively apply the above steps to the sub-arrays on the left and right of the pivot",
+ "The base case is when a sub-array has 0 or 1 elements (already sorted)"
+ ],
+ "best-case": "When the pivot always divides the array into nearly equal halves, quick sort achieves its best performance.",
+ "average-case": "For random arrays, quick sort is very efficient with O(n log n) time complexity.",
+ "worst-case": "When the pivot is always the smallest or largest element (e.g., in already sorted arrays), quick sort degrades to O(n²) performance."
+ }
}
}
diff --git a/messages/sk.json b/messages/sk.json
index b908896..3d946de 100644
--- a/messages/sk.json
+++ b/messages/sk.json
@@ -71,5 +71,80 @@
"current": "Aktuálny prvok",
"sorted": "Vytriedené",
"length": "Dĺžka"
+ },
+ "algorithm-explanation": {
+ "stats": {
+ "title": "Štatistiky",
+ "iterations": "Iterácie",
+ "swaps": "Výmeny"
+ },
+ "complexity": {
+ "title": "Analýza zložitosti",
+ "case": "Prípad",
+ "best": "Najlepší",
+ "average": "Priemerný",
+ "worst": "Najhorší",
+ "time": "Čas",
+ "space": "Priestor"
+ },
+ "explanation": {
+ "title": "Ako to funguje",
+ "steps": "Kroky",
+ "cases": "Prípady",
+ "code": "Kód"
+ },
+ "bubble": {
+ "title": "Bubble Sort",
+ "description": "Jednoduchý porovnávací algoritmus, ktorý opakovane prechádza zoznamom, porovnáva susedné prvky a vymieňa ich, ak sú v nesprávnom poradí.",
+ "steps": [
+ "Začnite na začiatku poľa",
+ "Porovnajte susedné prvky. Ak je prvý väčší ako druhý, vymeňte ich",
+ "Prejdite na ďalší pár prvkov a opakujte porovnanie a výmenu, ak je to potrebné",
+ "Po jednom úplnom prechode bude najväčší prvok na konci. Opakujte proces pre zostávajúce prvky"
+ ],
+ "best-case": "Keď je pole už zoradené, bubble sort urobí iba jeden prechod poľom bez výmen.",
+ "average-case": "Pre náhodné polia bubble sort stále vyžaduje kvadratický čas.",
+ "worst-case": "Keď je pole zoradené v opačnom poradí, bubble sort musí urobiť maximálny počet porovnaní a výmen."
+ },
+ "selection": {
+ "title": "Selection Sort",
+ "description": "Jednoduchý porovnávací algoritmus triedenia na mieste, ktorý rozdeľuje vstupný zoznam na dve časti: zoradenú časť a nezoradenú časť.",
+ "steps": [
+ "Nájdite minimálny prvok v nezotriedenej časti poľa",
+ "Vymeňte ho s prvkom na začiatku nezotriedenej časti",
+ "Posuňte hranicu medzi zotriedenou a nezotriedenou časťou o jeden prvok doprava",
+ "Opakujte, kým nie je celé pole zotriedené"
+ ],
+ "best-case": "Selection sort vždy vykonáva rovnaký počet porovnaní bez ohľadu na poradie vstupného poľa.",
+ "average-case": "Pre náhodné polia selection sort vyžaduje kvadratický čas.",
+ "worst-case": "Selection sort vždy vykonáva rovnaký počet porovnaní a výmen bez ohľadu na poradie vstupného poľa."
+ },
+ "insertion": {
+ "title": "Insertion Sort",
+ "description": "Jednoduchý triediaci algoritmus, ktorý buduje konečné zoradené pole po jednom prvku, podobne ako by ste mohli triediť hracie karty v ruke.",
+ "steps": [
+ "Začnite s druhým prvkom (predpokladajte, že prvý prvok je už zotriedený)",
+ "Porovnajte aktuálny prvok s predchádzajúcimi prvkami",
+ "Ak je predchádzajúci prvok väčší ako aktuálny prvok, posuňte predchádzajúci prvok o jednu pozíciu dopredu",
+ "Opakujte, kým sa nenájde správna pozícia pre aktuálny prvok, potom ho vložte"
+ ],
+ "best-case": "Keď je pole už zoradené, insertion sort robí iba jedno porovnanie na prvok.",
+ "average-case": "Pre náhodné polia insertion sort vyžaduje kvadratický čas.",
+ "worst-case": "Keď je pole zoradené v opačnom poradí, insertion sort musí posunúť každý prvok na začiatok poľa."
+ },
+ "quicksort": {
+ "title": "Quick Sort",
+ "description": "Efektívny algoritmus triedenia typu rozdeľuj a panuj, ktorý funguje výberom 'pivotného' prvku a rozdelením poľa okolo pivotu.",
+ "steps": [
+ "Vyberte pivotný prvok z poľa",
+ "Rozdeľte pole: prvky menšie ako pivot idú doľava, väčšie doprava",
+ "Pivot je teraz na svojej konečnej zotriedenej pozícii",
+ "Rekurzívne aplikujte vyššie uvedené kroky na podpolia naľavo a napravo od pivotu",
+ "Základný prípad je, keď podpole má 0 alebo 1 prvok (už zotriedené)"
+ ],
+ "best-case": "Keď pivot vždy rozdelí pole na takmer rovnaké polovice, quick sort dosahuje najlepší výkon.",
+ "average-case": "Pre náhodné polia je quick sort veľmi efektívny s časovou zložitosťou O(n log n).",
+ "worst-case": "Keď je pivot vždy najmenší alebo najväčší prvok (napr. v už zoradených poliach), výkon quick sortu sa zhoršuje na O(n²)."
+ }
}
}
diff --git a/src/app/[locale]/playground/page.tsx b/src/app/[locale]/playground/page.tsx
index 3df85f5..7d3cead 100644
--- a/src/app/[locale]/playground/page.tsx
+++ b/src/app/[locale]/playground/page.tsx
@@ -1,13 +1,42 @@
+"use client";
+
+import AlgorithmExplanation from "@/components/algorithm-explanation";
import { HexagonBackground } from "@/components/animate-ui/hexagon-background";
import SortingVisualizer from "@/components/sorting-visualizer";
+import { useState } from "react";
export default function Playground() {
+ const [algorithm, setAlgorithm] = useState<
+ "bubble" | "selection" | "insertion" | "quicksort"
+ >("bubble");
+ const [iterations, setIterations] = useState(0);
+ const [swaps, setSwaps] = useState(0);
+
return (
-
-
+
+
-
);
diff --git a/src/components/algorithm-explanation.tsx b/src/components/algorithm-explanation.tsx
new file mode 100644
index 0000000..99568b1
--- /dev/null
+++ b/src/components/algorithm-explanation.tsx
@@ -0,0 +1,357 @@
+"use client";
+
+import { useTranslations } from "next-intl";
+import {
+ Tabs,
+ TabsContent,
+ TabsContents,
+ TabsList,
+ TabsTrigger,
+} from "@/components/animate-ui/tabs";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { cn } from "@/lib/utils";
+
+interface AlgorithmExplanationProps {
+ algorithm: "bubble" | "selection" | "insertion" | "quicksort";
+ iterations: number;
+ swaps: number;
+ className?: string;
+}
+
+export default function AlgorithmExplanation({
+ algorithm,
+ iterations,
+ swaps,
+ className,
+}: AlgorithmExplanationProps) {
+ const t = useTranslations("algorithm-explanation");
+
+ const algorithmData = {
+ bubble: {
+ title: t("bubble.title"),
+ description: t("bubble.description"),
+ steps: [
+ t("bubble.steps.0"),
+ t("bubble.steps.1"),
+ t("bubble.steps.2"),
+ t("bubble.steps.3"),
+ ],
+ timeComplexity: {
+ best: "O(n)",
+ average: "O(n²)",
+ worst: "O(n²)",
+ },
+ spaceComplexity: "O(1)",
+ bestCase: t("bubble.best-case"),
+ averageCase: t("bubble.average-case"),
+ worstCase: t("bubble.worst-case"),
+ },
+ selection: {
+ title: t("selection.title"),
+ description: t("selection.description"),
+ steps: [
+ t("selection.steps.0"),
+ t("selection.steps.1"),
+ t("selection.steps.2"),
+ t("selection.steps.3"),
+ ],
+ timeComplexity: {
+ best: "O(n²)",
+ average: "O(n²)",
+ worst: "O(n²)",
+ },
+ spaceComplexity: "O(1)",
+ bestCase: t("selection.best-case"),
+ averageCase: t("selection.average-case"),
+ worstCase: t("selection.worst-case"),
+ },
+ insertion: {
+ title: t("insertion.title"),
+ description: t("insertion.description"),
+ steps: [
+ t("insertion.steps.0"),
+ t("insertion.steps.1"),
+ t("insertion.steps.2"),
+ t("insertion.steps.3"),
+ ],
+ timeComplexity: {
+ best: "O(n)",
+ average: "O(n²)",
+ worst: "O(n²)",
+ },
+ spaceComplexity: "O(1)",
+ bestCase: t("insertion.best-case"),
+ averageCase: t("insertion.average-case"),
+ worstCase: t("insertion.worst-case"),
+ },
+ quicksort: {
+ title: t("quicksort.title"),
+ description: t("quicksort.description"),
+ steps: [
+ t("quicksort.steps.0"),
+ t("quicksort.steps.1"),
+ t("quicksort.steps.2"),
+ t("quicksort.steps.3"),
+ t("quicksort.steps.4"),
+ ],
+ timeComplexity: {
+ best: "O(n log n)",
+ average: "O(n log n)",
+ worst: "O(n²)",
+ },
+ spaceComplexity: "O(log n)",
+ bestCase: t("quicksort.best-case"),
+ averageCase: t("quicksort.average-case"),
+ worstCase: t("quicksort.worst-case"),
+ },
+ };
+
+ const currentAlgorithm = algorithmData[algorithm];
+
+ return (
+
+
+
+ {currentAlgorithm.title}
+ {currentAlgorithm.description}
+
+
+
+ {/* Stats Section */}
+
+
{t("stats.title")}
+
+
+
+
+ {t("stats.iterations")}
+
+
+ {iterations}
+
+
+
+
+ {t("stats.swaps")}
+
+
{swaps}
+
+
+
+
+
{t("complexity.title")}
+
+
+
{t("complexity.case")}
+
{t("complexity.best")}
+
{t("complexity.average")}
+
{t("complexity.worst")}
+
+
+
{t("complexity.time")}
+
+ {currentAlgorithm.timeComplexity.best}
+
+
+ {currentAlgorithm.timeComplexity.average}
+
+
+ {currentAlgorithm.timeComplexity.worst}
+
+
+
+
{t("complexity.space")}
+
+ {currentAlgorithm.spaceComplexity}
+
+
+
+
+
+
+ {/* Explanation Section */}
+
+
+ {t("explanation.title")}
+
+
+
+
+
+ {t("explanation.steps")}
+
+
+ {t("explanation.cases")}
+
+
+ {t("explanation.code")}
+
+
+
+
+
+
+ {currentAlgorithm.steps.map((step, index) => (
+ {step}
+ ))}
+
+
+
+
+
+
+
+ {t("complexity.best")} (
+ {currentAlgorithm.timeComplexity.best})
+
+
+ {currentAlgorithm.bestCase}
+
+
+
+
+ {t("complexity.average")} (
+ {currentAlgorithm.timeComplexity.average})
+
+
+ {currentAlgorithm.averageCase}
+
+
+
+
+ {t("complexity.worst")} (
+ {currentAlgorithm.timeComplexity.worst})
+
+
+ {currentAlgorithm.worstCase}
+
+
+
+
+
+
+
+ {getAlgorithmCode(algorithm)}
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function getAlgorithmCode(algorithm: string): string {
+ switch (algorithm) {
+ case "bubble":
+ return `function bubbleSort(arr) {
+ const n = arr.length;
+
+ for (let i = 0; i < n - 1; i++) {
+ let swapped = false;
+
+ for (let j = 0; j < n - i - 1; j++) {
+ if (arr[j] > arr[j + 1]) {
+ // Swap elements
+ [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
+ swapped = true;
+ }
+ }
+
+ // If no swapping occurred in this pass,
+ // array is sorted
+ if (!swapped) break;
+ }
+
+ return arr;
+}`;
+ case "selection":
+ return `function selectionSort(arr) {
+ const n = arr.length;
+
+ for (let i = 0; i < n - 1; i++) {
+ // Find minimum element in unsorted part
+ let minIndex = i;
+
+ for (let j = i + 1; j < n; j++) {
+ if (arr[j] < arr[minIndex]) {
+ minIndex = j;
+ }
+ }
+
+ // Swap found minimum with first element
+ if (minIndex !== i) {
+ [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
+ }
+ }
+
+ return arr;
+}`;
+ case "insertion":
+ return `function insertionSort(arr) {
+ const n = arr.length;
+
+ for (let i = 1; i < n; i++) {
+ // Store current element to be inserted
+ let key = arr[i];
+ let j = i - 1;
+
+ // Move elements greater than key
+ // to one position ahead
+ while (j >= 0 && arr[j] > key) {
+ arr[j + 1] = arr[j];
+ j--;
+ }
+
+ // Insert the key at correct position
+ arr[j + 1] = key;
+ }
+
+ return arr;
+}`;
+ case "quicksort":
+ return `function quickSort(arr, low = 0, high = arr.length - 1) {
+ if (low < high) {
+ // Find pivot element such that
+ // elements smaller than pivot are on the left
+ // elements greater than pivot are on the right
+ const pivotIndex = partition(arr, low, high);
+
+ // Recursively sort elements before and after pivot
+ quickSort(arr, low, pivotIndex - 1);
+ quickSort(arr, pivotIndex + 1, high);
+ }
+
+ return arr;
+}
+
+function partition(arr, low, high) {
+ // Choose rightmost element as pivot
+ const pivot = arr[high];
+ let i = low - 1;
+
+ // Compare each element with pivot
+ for (let j = low; j < high; j++) {
+ if (arr[j] <= pivot) {
+ i++;
+ [arr[i], arr[j]] = [arr[j], arr[i]];
+ }
+ }
+
+ // Place pivot in its final position
+ [arr[i + 1], arr[high]] = [arr[high], arr[i + 1]];
+ return i + 1;
+}`;
+ default:
+ return "";
+ }
+}
diff --git a/src/components/animate-ui/motion-highlight.tsx b/src/components/animate-ui/motion-highlight.tsx
new file mode 100644
index 0000000..680a43c
--- /dev/null
+++ b/src/components/animate-ui/motion-highlight.tsx
@@ -0,0 +1,586 @@
+"use client";
+
+import { AnimatePresence, motion, type Transition } from "motion/react";
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+type MotionHighlightMode = "children" | "parent";
+
+type Bounds = {
+ top: number;
+ left: number;
+ width: number;
+ height: number;
+};
+
+type MotionHighlightContextType = {
+ mode: MotionHighlightMode;
+ activeValue: string | null;
+ setActiveValue: (value: string | null) => void;
+ setBounds: (bounds: DOMRect) => void;
+ clearBounds: () => void;
+ id: string;
+ hover: boolean;
+ className?: string;
+ activeClassName?: string;
+ setActiveClassName: (className: string) => void;
+ transition?: Transition;
+ disabled?: boolean;
+ enabled?: boolean;
+ exitDelay?: number;
+ forceUpdateBounds?: boolean;
+};
+
+const MotionHighlightContext = React.createContext<
+ MotionHighlightContextType | undefined
+>(undefined);
+
+const useMotionHighlight = (): MotionHighlightContextType => {
+ const context = React.useContext(MotionHighlightContext);
+ if (!context) {
+ throw new Error(
+ "useMotionHighlight must be used within a MotionHighlightProvider",
+ );
+ }
+ return context;
+};
+
+type BaseMotionHighlightProps = {
+ mode?: MotionHighlightMode;
+ value?: string | null;
+ defaultValue?: string | null;
+ onValueChange?: (value: string | null) => void;
+ className?: string;
+ transition?: Transition;
+ hover?: boolean;
+ disabled?: boolean;
+ enabled?: boolean;
+ exitDelay?: number;
+};
+
+type ParentModeMotionHighlightProps = {
+ boundsOffset?: Partial
;
+ containerClassName?: string;
+ forceUpdateBounds?: boolean;
+};
+
+type ControlledParentModeMotionHighlightProps = BaseMotionHighlightProps &
+ ParentModeMotionHighlightProps & {
+ mode: "parent";
+ controlledItems: true;
+ children: React.ReactNode;
+ };
+
+type ControlledChildrenModeMotionHighlightProps = BaseMotionHighlightProps & {
+ mode?: "children" | undefined;
+ controlledItems: true;
+ children: React.ReactNode;
+};
+
+type UncontrolledParentModeMotionHighlightProps = BaseMotionHighlightProps &
+ ParentModeMotionHighlightProps & {
+ mode: "parent";
+ controlledItems?: false;
+ itemsClassName?: string;
+ children: React.ReactElement | React.ReactElement[];
+ };
+
+type UncontrolledChildrenModeMotionHighlightProps = BaseMotionHighlightProps & {
+ mode?: "children";
+ controlledItems?: false;
+ itemsClassName?: string;
+ children: React.ReactElement | React.ReactElement[];
+};
+
+type MotionHighlightProps = React.ComponentProps<"div"> &
+ (
+ | ControlledParentModeMotionHighlightProps
+ | ControlledChildrenModeMotionHighlightProps
+ | UncontrolledParentModeMotionHighlightProps
+ | UncontrolledChildrenModeMotionHighlightProps
+ );
+
+function MotionHighlight({ ref, ...props }: MotionHighlightProps) {
+ const {
+ children,
+ value,
+ defaultValue,
+ onValueChange,
+ className,
+ transition = { type: "spring", stiffness: 350, damping: 35 },
+ hover = false,
+ enabled = true,
+ controlledItems,
+ disabled = false,
+ exitDelay = 0.2,
+ mode = "children",
+ } = props;
+
+ const localRef = React.useRef(null);
+ React.useImperativeHandle(ref, () => localRef.current as HTMLDivElement);
+
+ const [activeValue, setActiveValue] = React.useState(
+ value ?? defaultValue ?? null,
+ );
+ const [boundsState, setBoundsState] = React.useState(null);
+ const [activeClassNameState, setActiveClassNameState] =
+ React.useState("");
+
+ const safeSetActiveValue = React.useCallback(
+ (id: string | null) => {
+ setActiveValue(prev => (prev === id ? prev : id));
+ if (id !== activeValue) onValueChange?.(id);
+ },
+ [activeValue, onValueChange],
+ );
+
+ const safeSetBounds = React.useCallback(
+ (bounds: DOMRect) => {
+ if (!localRef.current) return;
+
+ const boundsOffset = (props as ParentModeMotionHighlightProps)
+ ?.boundsOffset ?? {
+ top: 0,
+ left: 0,
+ width: 0,
+ height: 0,
+ };
+
+ const containerRect = localRef.current.getBoundingClientRect();
+ const newBounds: Bounds = {
+ top: bounds.top - containerRect.top + (boundsOffset.top ?? 0),
+ left: bounds.left - containerRect.left + (boundsOffset.left ?? 0),
+ width: bounds.width + (boundsOffset.width ?? 0),
+ height: bounds.height + (boundsOffset.height ?? 0),
+ };
+
+ setBoundsState(prev => {
+ if (
+ prev &&
+ prev.top === newBounds.top &&
+ prev.left === newBounds.left &&
+ prev.width === newBounds.width &&
+ prev.height === newBounds.height
+ ) {
+ return prev;
+ }
+ return newBounds;
+ });
+ },
+ // biome-ignore lint/correctness/useExhaustiveDependencies: Not needed with React Compiler (https://github.com/biomejs/biome/issues/5293)
+ [props],
+ );
+
+ const clearBounds = React.useCallback(() => {
+ setBoundsState(prev => (prev === null ? prev : null));
+ }, []);
+
+ React.useEffect(() => {
+ if (value !== undefined) setActiveValue(value);
+ else if (defaultValue !== undefined) setActiveValue(defaultValue);
+ }, [value, defaultValue]);
+
+ const id = React.useId();
+
+ React.useEffect(() => {
+ if (mode !== "parent") return;
+ const container = localRef.current;
+ if (!container) return;
+
+ const onScroll = () => {
+ if (!activeValue) return;
+ const activeEl = container.querySelector(
+ `[data-value="${activeValue}"][data-highlight="true"]`,
+ );
+ if (activeEl) safeSetBounds(activeEl.getBoundingClientRect());
+ };
+
+ container.addEventListener("scroll", onScroll, { passive: true });
+ return () => container.removeEventListener("scroll", onScroll);
+ }, [mode, activeValue, safeSetBounds]);
+
+ const render = React.useCallback(
+ (children: React.ReactNode) => {
+ if (mode === "parent") {
+ return (
+
+
+ {boundsState && (
+
+ )}
+
+ {children}
+
+ );
+ }
+
+ return children;
+ },
+ [
+ mode,
+ // biome-ignore lint/correctness/useExhaustiveDependencies: Not needed with React Compiler (https://github.com/biomejs/biome/issues/5293)
+ props,
+ boundsState,
+ transition,
+ exitDelay,
+ className,
+ activeClassNameState,
+ ],
+ );
+
+ return (
+
+ {enabled
+ ? controlledItems
+ ? render(children)
+ : render(
+ React.Children.map(children, (child, index) => (
+
+ {child}
+
+ )),
+ )
+ : children}
+
+ );
+}
+
+function getNonOverridingDataAttributes(
+ element: React.ReactElement,
+ dataAttributes: Record,
+): Record {
+ return Object.keys(dataAttributes).reduce>(
+ (acc, key) => {
+ if ((element.props as Record)[key] === undefined) {
+ acc[key] = dataAttributes[key];
+ }
+ return acc;
+ },
+ {},
+ );
+}
+
+type ExtendedChildProps = React.ComponentProps<"div"> & {
+ id?: string;
+ ref?: React.Ref;
+ "data-active"?: string;
+ "data-value"?: string;
+ "data-disabled"?: boolean;
+ "data-highlight"?: boolean;
+ "data-slot"?: string;
+};
+
+type MotionHighlightItemProps = React.ComponentProps<"div"> & {
+ children: React.ReactElement;
+ id?: string;
+ value?: string;
+ className?: string;
+ transition?: Transition;
+ activeClassName?: string;
+ disabled?: boolean;
+ exitDelay?: number;
+ asChild?: boolean;
+ forceUpdateBounds?: boolean;
+};
+
+function MotionHighlightItem({
+ ref,
+ children,
+ id,
+ value,
+ className,
+ transition,
+ disabled = false,
+ activeClassName,
+ exitDelay,
+ asChild = false,
+ forceUpdateBounds,
+ ...props
+}: MotionHighlightItemProps) {
+ const itemId = React.useId();
+ const {
+ activeValue,
+ setActiveValue,
+ mode,
+ setBounds,
+ clearBounds,
+ hover,
+ enabled,
+ className: contextClassName,
+ transition: contextTransition,
+ id: contextId,
+ disabled: contextDisabled,
+ exitDelay: contextExitDelay,
+ forceUpdateBounds: contextForceUpdateBounds,
+ setActiveClassName,
+ } = useMotionHighlight();
+
+ const element = children as React.ReactElement;
+ const childValue =
+ id ?? value ?? element.props?.["data-value"] ?? element.props?.id ?? itemId;
+ const isActive = activeValue === childValue;
+ const isDisabled = disabled === undefined ? contextDisabled : disabled;
+ const itemTransition = transition ?? contextTransition;
+
+ const localRef = React.useRef(null);
+ React.useImperativeHandle(ref, () => localRef.current as HTMLDivElement);
+
+ React.useEffect(() => {
+ if (mode !== "parent") return;
+ let rafId: number;
+ let previousBounds: Bounds | null = null;
+ const shouldUpdateBounds =
+ forceUpdateBounds === true ||
+ (contextForceUpdateBounds && forceUpdateBounds !== false);
+
+ const updateBounds = () => {
+ if (!localRef.current) return;
+
+ const bounds = localRef.current.getBoundingClientRect();
+
+ if (shouldUpdateBounds) {
+ if (
+ previousBounds &&
+ previousBounds.top === bounds.top &&
+ previousBounds.left === bounds.left &&
+ previousBounds.width === bounds.width &&
+ previousBounds.height === bounds.height
+ ) {
+ rafId = requestAnimationFrame(updateBounds);
+ return;
+ }
+ previousBounds = bounds;
+ rafId = requestAnimationFrame(updateBounds);
+ }
+
+ setBounds(bounds);
+ };
+
+ if (isActive) {
+ updateBounds();
+ setActiveClassName(activeClassName ?? "");
+ } else if (!activeValue) clearBounds();
+
+ if (shouldUpdateBounds) return () => cancelAnimationFrame(rafId);
+ }, [
+ mode,
+ isActive,
+ activeValue,
+ setBounds,
+ clearBounds,
+ activeClassName,
+ setActiveClassName,
+ forceUpdateBounds,
+ contextForceUpdateBounds,
+ ]);
+
+ if (!React.isValidElement(children)) return children;
+
+ const dataAttributes = {
+ "data-active": isActive ? "true" : "false",
+ "aria-selected": isActive,
+ "data-disabled": isDisabled,
+ "data-value": childValue,
+ "data-highlight": true,
+ };
+
+ const commonHandlers = hover
+ ? {
+ onMouseEnter: (e: React.MouseEvent) => {
+ setActiveValue(childValue);
+ element.props.onMouseEnter?.(e);
+ },
+ onMouseLeave: (e: React.MouseEvent) => {
+ setActiveValue(null);
+ element.props.onMouseLeave?.(e);
+ },
+ }
+ : {
+ onClick: (e: React.MouseEvent) => {
+ setActiveValue(childValue);
+ element.props.onClick?.(e);
+ },
+ };
+
+ if (asChild) {
+ if (mode === "children") {
+ return React.cloneElement(
+ element,
+ {
+ key: childValue,
+ ref: localRef,
+ className: cn("relative", element.props.className),
+ ...getNonOverridingDataAttributes(element, {
+ ...dataAttributes,
+ "data-slot": "motion-highlight-item-container",
+ }),
+ ...commonHandlers,
+ ...props,
+ },
+ <>
+
+ {isActive && !isDisabled && (
+
+ )}
+
+
+
+ {children}
+
+ >,
+ );
+ }
+
+ return React.cloneElement(element, {
+ ref: localRef,
+ ...getNonOverridingDataAttributes(element, {
+ ...dataAttributes,
+ "data-slot": "motion-highlight-item",
+ }),
+ ...commonHandlers,
+ });
+ }
+
+ return enabled ? (
+
+ {mode === "children" && (
+
+ {isActive && !isDisabled && (
+
+ )}
+
+ )}
+
+ {React.cloneElement(element, {
+ className: cn("relative z-[1]", element.props.className),
+ ...getNonOverridingDataAttributes(element, {
+ ...dataAttributes,
+ "data-slot": "motion-highlight-item",
+ }),
+ })}
+
+ ) : (
+ children
+ );
+}
+
+export {
+ MotionHighlight,
+ MotionHighlightItem,
+ useMotionHighlight,
+ type MotionHighlightProps,
+ type MotionHighlightItemProps,
+};
diff --git a/src/components/animate-ui/tabs.tsx b/src/components/animate-ui/tabs.tsx
new file mode 100644
index 0000000..66a6e47
--- /dev/null
+++ b/src/components/animate-ui/tabs.tsx
@@ -0,0 +1,282 @@
+"use client";
+
+import { type HTMLMotionProps, motion, type Transition } from "motion/react";
+import * as React from "react";
+import {
+ MotionHighlight,
+ MotionHighlightItem,
+} from "@/components/animate-ui/motion-highlight";
+import { cn } from "@/lib/utils";
+
+type TabsContextType = {
+ activeValue: string;
+ handleValueChange: (value: string) => void;
+ registerTrigger: (value: string, node: HTMLElement | null) => void;
+};
+
+const TabsContext = React.createContext(undefined);
+
+const useTabs = (): TabsContextType => {
+ const context = React.useContext(TabsContext);
+ if (!context) {
+ throw new Error("useTabs must be used within a TabsProvider");
+ }
+ return context;
+};
+
+type BaseTabsProps = React.ComponentProps<"div"> & {
+ children: React.ReactNode;
+};
+
+type UnControlledTabsProps = BaseTabsProps & {
+ defaultValue?: string;
+ value?: never;
+ onValueChange?: never;
+};
+
+type ControlledTabsProps = BaseTabsProps & {
+ value: string;
+ onValueChange?: (value: string) => void;
+ defaultValue?: never;
+};
+
+type TabsProps = UnControlledTabsProps | ControlledTabsProps;
+
+function Tabs({
+ defaultValue,
+ value,
+ onValueChange,
+ children,
+ className,
+ ...props
+}: TabsProps) {
+ const [activeValue, setActiveValue] = React.useState(defaultValue);
+ const triggersRef = React.useRef(new Map());
+ const initialSet = React.useRef(false);
+ const isControlled = value !== undefined;
+
+ React.useEffect(() => {
+ if (
+ !isControlled &&
+ activeValue === undefined &&
+ triggersRef.current.size > 0 &&
+ !initialSet.current
+ ) {
+ const firstTab = Array.from(triggersRef.current.keys())[0];
+ setActiveValue(firstTab);
+ initialSet.current = true;
+ }
+ }, [activeValue, isControlled]);
+
+ const registerTrigger = (value: string, node: HTMLElement | null) => {
+ if (node) {
+ triggersRef.current.set(value, node);
+ if (!isControlled && activeValue === undefined && !initialSet.current) {
+ setActiveValue(value);
+ initialSet.current = true;
+ }
+ } else {
+ triggersRef.current.delete(value);
+ }
+ };
+
+ const handleValueChange = (val: string) => {
+ if (!isControlled) setActiveValue(val);
+ else onValueChange?.(val);
+ };
+
+ return (
+
+
+ {children}
+
+
+ );
+}
+
+type TabsListProps = React.ComponentProps<"div"> & {
+ children: React.ReactNode;
+ activeClassName?: string;
+ transition?: Transition;
+};
+
+function TabsList({
+ children,
+ className,
+ activeClassName,
+ transition = {
+ type: "spring",
+ stiffness: 200,
+ damping: 25,
+ },
+ ...props
+}: TabsListProps) {
+ const { activeValue } = useTabs();
+
+ return (
+
+
+ {children}
+
+
+ );
+}
+
+type TabsTriggerProps = HTMLMotionProps<"button"> & {
+ value: string;
+ children: React.ReactNode;
+};
+
+function TabsTrigger({
+ ref,
+ value,
+ children,
+ className,
+ ...props
+}: TabsTriggerProps) {
+ const { activeValue, handleValueChange, registerTrigger } = useTabs();
+
+ const localRef = React.useRef(null);
+ React.useImperativeHandle(ref, () => localRef.current as HTMLButtonElement);
+
+ React.useEffect(() => {
+ registerTrigger(value, localRef.current);
+ return () => registerTrigger(value, null);
+ }, [value, registerTrigger]);
+
+ return (
+
+ handleValueChange(value)}
+ data-state={activeValue === value ? "active" : "inactive"}
+ className={cn(
+ "z-[1] inline-flex size-full cursor-pointer items-center justify-center whitespace-nowrap rounded-sm px-2 py-1 font-medium text-sm ring-offset-background transition-transform focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:text-foreground",
+ className,
+ )}
+ {...props}
+ >
+ {children}
+
+
+ );
+}
+
+type TabsContentsProps = React.ComponentProps<"div"> & {
+ children: React.ReactNode;
+ transition?: Transition;
+};
+
+function TabsContents({
+ children,
+ className,
+ transition = {
+ type: "spring",
+ stiffness: 300,
+ damping: 30,
+ bounce: 0,
+ restDelta: 0.01,
+ },
+ ...props
+}: TabsContentsProps) {
+ const { activeValue } = useTabs();
+ const childrenArray = React.Children.toArray(children);
+ const activeIndex = childrenArray.findIndex(
+ (child): child is React.ReactElement<{ value: string }> =>
+ React.isValidElement(child) &&
+ typeof child.props === "object" &&
+ child.props !== null &&
+ "value" in child.props &&
+ child.props.value === activeValue,
+ );
+
+ return (
+
+
+ {childrenArray.map((child, index) => (
+
+ {child}
+
+ ))}
+
+
+ );
+}
+
+type TabsContentProps = HTMLMotionProps<"div"> & {
+ value: string;
+ children: React.ReactNode;
+};
+
+function TabsContent({
+ children,
+ value,
+ className,
+ ...props
+}: TabsContentProps) {
+ const { activeValue } = useTabs();
+ const isActive = activeValue === value;
+ return (
+
+ {children}
+
+ );
+}
+
+export {
+ Tabs,
+ TabsList,
+ TabsTrigger,
+ TabsContents,
+ TabsContent,
+ useTabs,
+ type TabsContextType,
+ type TabsProps,
+ type TabsListProps,
+ type TabsTriggerProps,
+ type TabsContentsProps,
+ type TabsContentProps,
+};
diff --git a/src/components/sorting-visualizer.tsx b/src/components/sorting-visualizer.tsx
index 44ce661..1422f41 100644
--- a/src/components/sorting-visualizer.tsx
+++ b/src/components/sorting-visualizer.tsx
@@ -25,8 +25,18 @@ type BarData = {
};
export default function SortingVisualizer({
+ algorithm,
+ setAlgorithm,
+ setIterations,
+ setSwaps,
className,
}: {
+ algorithm: "bubble" | "selection" | "insertion" | "quicksort";
+ setAlgorithm: (
+ algorithm: "bubble" | "selection" | "insertion" | "quicksort",
+ ) => void;
+ setIterations: (val: ((prev: number) => number) | number) => void;
+ setSwaps: (val: ((prev: number) => number) | number) => void;
className?: string;
}) {
const t = useTranslations("visualizer");
@@ -45,10 +55,6 @@ export default function SortingVisualizer({
// A ref is needed because the the state in useState would be encapsulated in the sorting algorithm closures
const speedRef = useRef(speed);
- const [algorithm, setAlgorithm] = useState<
- "bubble" | "selection" | "insertion" | "quicksort"
- >("bubble");
-
const svgRef = useRef(null);
const animationFrameId = useRef(null);
@@ -250,6 +256,7 @@ export default function SortingVisualizer({
// Mark elements being compared
workingData[j]!.state = "comparing";
workingData[j + 1]!.state = "comparing";
+ setIterations(prev => prev + 1);
setData([...workingData]);
await sleep(getDelay());
@@ -263,6 +270,7 @@ export default function SortingVisualizer({
// Update visualization
setData([...workingData]);
+ setSwaps(prev => prev + 1);
await sleep(getDelay());
}
@@ -320,6 +328,7 @@ export default function SortingVisualizer({
let minIdx = i;
workingData[i]!.state = "comparing";
setData([...workingData]);
+ setIterations(prev => prev + 1);
await sleep(getDelay());
// Find the minimum element in the unsorted part
@@ -332,6 +341,7 @@ export default function SortingVisualizer({
// Mark current element being compared
workingData[j]!.state = "comparing";
setData([...workingData]);
+ setIterations(prev => prev + 1);
await sleep(getDelay());
if (workingData[j]!.value < workingData[minIdx]!.value) {
@@ -366,6 +376,7 @@ export default function SortingVisualizer({
// Update visualization
setData([...workingData]);
+ setSwaps(prev => prev + 1);
await sleep(getDelay());
}
@@ -414,6 +425,7 @@ export default function SortingVisualizer({
// Mark the first element as sorted initially
workingData[0]!.state = "sorted";
setData([...workingData]);
+ setIterations(prev => prev + 1);
await sleep(getDelay());
// Main insertion sort loop
@@ -431,6 +443,7 @@ export default function SortingVisualizer({
// Mark current element
workingData[i]!.state = "comparing";
setData([...workingData]);
+ setIterations(prev => prev + 1);
await sleep(getDelay());
// Find the position to insert the current element
@@ -456,6 +469,8 @@ export default function SortingVisualizer({
// Update visualization with the shift
setData([...workingData]);
+ setIterations(prev => prev + 1);
+ setSwaps(prev => prev + 1);
await sleep(getDelay());
}
@@ -537,6 +552,7 @@ export default function SortingVisualizer({
// Mark current element being compared
workingData[j]!.state = "comparing";
setData([...workingData]);
+ setIterations(prev => prev + 1);
await sleep(getDelay());
if (workingData[j]!.value < pivotValue) {
@@ -554,6 +570,7 @@ export default function SortingVisualizer({
workingData[j] = temp;
setData([...workingData]);
+ setSwaps(prev => prev + 1);
await sleep(getDelay());
}
}
@@ -578,6 +595,7 @@ export default function SortingVisualizer({
// Update visualization
setData([...workingData]);
+ setSwaps(prev => prev + 1);
await sleep(getDelay());
// Reset states except for the pivot position
@@ -600,11 +618,13 @@ export default function SortingVisualizer({
workingData[left]!.state = "comparing";
workingData[mid]!.state = "comparing";
setData([...workingData]);
+ setIterations(prev => prev + 1);
await sleep(getDelay());
if (workingData[left]!.value > workingData[mid]!.value) {
const temp = workingData[left]!;
workingData[left] = workingData[mid]!;
workingData[mid] = temp;
+ setSwaps(prev => prev + 1);
}
workingData[left]!.state = "default";
workingData[mid]!.state = "default";
@@ -614,11 +634,13 @@ export default function SortingVisualizer({
workingData[left]!.state = "comparing";
workingData[right]!.state = "comparing";
setData([...workingData]);
+ setIterations(prev => prev + 1);
await sleep(getDelay());
if (workingData[left]!.value > workingData[right]!.value) {
const temp = workingData[left]!;
workingData[left] = workingData[right]!;
workingData[right] = temp;
+ setSwaps(prev => prev + 1);
}
workingData[left]!.state = "default";
workingData[right]!.state = "default";
@@ -628,11 +650,13 @@ export default function SortingVisualizer({
workingData[mid]!.state = "comparing";
workingData[right]!.state = "comparing";
setData([...workingData]);
+ setIterations(prev => prev + 1);
await sleep(getDelay());
if (workingData[mid]!.value > workingData[right]!.value) {
const temp = workingData[mid]!;
workingData[mid] = workingData[right]!;
workingData[right] = temp;
+ setSwaps(prev => prev + 1);
}
workingData[mid]!.state = "default";
workingData[right]!.state = "default";
@@ -642,12 +666,14 @@ export default function SortingVisualizer({
workingData[mid]!.state = "comparing";
workingData[right]!.state = "comparing";
setData([...workingData]);
+ setIterations(prev => prev + 1);
await sleep(getDelay());
const temp = workingData[mid]!;
workingData[mid] = workingData[right]!;
workingData[right] = temp;
workingData[mid]!.state = "default";
workingData[right]!.state = "default";
+ setSwaps(prev => prev + 1);
setData([...workingData]);
await sleep(getDelay());
@@ -678,6 +704,8 @@ export default function SortingVisualizer({
setIsSorting(false);
setIsPaused(false);
+ setIterations(0);
+ setSwaps(0);
// Reset to original data with default states
const resetData = originalData.map(item => ({
@@ -701,6 +729,8 @@ export default function SortingVisualizer({
setIsSorting(false);
setIsPaused(false);
+ setIterations(0);
+ setSwaps(0);
// Generate new data
generateRandomData(arrayLength);
@@ -803,7 +833,9 @@ export default function SortingVisualizer({
- setAlgorithm(val as "bubble" | "selection" | "insertion")
+ setAlgorithm(
+ val as "bubble" | "selection" | "insertion" | "quicksort",
+ )
}
disabled={isSorting}
>
diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx
new file mode 100644
index 0000000..966d538
--- /dev/null
+++ b/src/components/ui/card.tsx
@@ -0,0 +1,92 @@
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+function Card({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function CardAction({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function CardContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+export {
+ Card,
+ CardHeader,
+ CardFooter,
+ CardTitle,
+ CardAction,
+ CardDescription,
+ CardContent,
+};