Basic Layout
Styled Layout
Installation
Copy and paste the following code into your project.
components/ui/art-board-layout.tsx
"use client";
import React, { useState, useRef, useEffect, useCallback } from "react";
import { cn } from "@/lib/utils";
interface ArtBoardLayoutProps {
gridSize?: number;
borderWidth?: "thin" | "medium" | "thick";
borderStyle?: "modern" | "classic" | "rustic";
backgroundColor?: string;
gridColor?: string;
snapToGrid?: boolean;
minHeight?: string;
showGridLines?: boolean;
className?: string;
children?: React.ReactNode;
}
interface Position {
x: number;
y: number;
}
interface DraggableProps {
children: React.ReactNode;
onPositionChange?: (position: Position) => void;
initialPosition?: Position;
gridSize?: number;
snapToGrid?: boolean;
}
const Draggable = ({
children,
onPositionChange,
initialPosition = { x: 0, y: 0 },
gridSize = 20,
snapToGrid = true,
}: DraggableProps) => {
const elementRef = useRef<HTMLDivElement>(null);
const [isDragging, setIsDragging] = useState(false);
const [position, setPosition] = useState<Position>(initialPosition);
const dragStart = useRef<Position>({ x: 0, y: 0 });
const elementStart = useRef<Position>({ x: 0, y: 0 });
const handleMouseDown = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
if (e.button !== 0) return; // Only left click
setIsDragging(true);
dragStart.current = { x: e.clientX, y: e.clientY };
elementStart.current = position;
e.stopPropagation();
},
[position],
);
const handleMouseMove = useCallback(
(e: MouseEvent) => {
if (!isDragging) return;
let newX = elementStart.current.x + (e.clientX - dragStart.current.x);
let newY = elementStart.current.y + (e.clientY - dragStart.current.y);
if (snapToGrid) {
newX = Math.round(newX / gridSize) * gridSize;
newY = Math.round(newY / gridSize) * gridSize;
}
const newPosition = { x: newX, y: newY };
setPosition(newPosition);
onPositionChange?.(newPosition);
},
[isDragging, gridSize, snapToGrid, onPositionChange],
);
const handleMouseUp = useCallback(() => {
setIsDragging(false);
}, []);
useEffect(() => {
if (isDragging) {
window.addEventListener("mousemove", handleMouseMove);
window.addEventListener("mouseup", handleMouseUp);
return () => {
window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("mouseup", handleMouseUp);
};
}
}, [isDragging, handleMouseMove, handleMouseUp]);
return (
<div
ref={elementRef}
style={{
position: "absolute",
transform: `translate(${position.x}px, ${position.y}px)`,
cursor: isDragging ? "grabbing" : "grab",
userSelect: "none",
zIndex: isDragging ? 1000 : 1,
}}
onMouseDown={handleMouseDown}
>
{children}
</div>
);
};
const borderStyles = {
modern: "bg-linear-to-br from-gray-50 to-gray-100 shadow-xs",
classic: "bg-linear-to-br from-amber-50 to-amber-100 shadow-inner",
rustic: "bg-linear-to-br from-stone-100 to-stone-200 shadow-inner",
};
const borderWidths = {
thin: "p-4",
medium: "p-6",
thick: "p-8",
};
export function ArtBoardLayout({
gridSize = 20,
borderWidth = "medium",
borderStyle = "modern",
backgroundColor = "white",
gridColor = "rgba(0, 0, 0, 0.05)",
snapToGrid = true,
minHeight = "400px",
showGridLines = true,
className,
children,
}: ArtBoardLayoutProps) {
const boardRef = useRef<HTMLDivElement>(null);
const [elements, setElements] = useState<React.ReactNode[]>([]);
useEffect(() => {
if (!children) return;
const childArray = React.Children.toArray(children);
setElements(
childArray.map((child, index) => (
<Draggable
key={index}
gridSize={gridSize}
snapToGrid={snapToGrid}
initialPosition={{ x: index * gridSize * 2, y: gridSize * 2 }}
>
{child}
</Draggable>
)),
);
}, [children, gridSize, snapToGrid]);
const gridBackground = showGridLines
? {
backgroundImage: `
linear-gradient(to right, ${gridColor} 1px, transparent 1px),
linear-gradient(to bottom, ${gridColor} 1px, transparent 1px)
`,
backgroundSize: `${gridSize}px ${gridSize}px`,
}
: {};
return (
<div
ref={boardRef}
className={cn(
"relative overflow-hidden rounded-xl border transition-colors",
"hover:shadow-lg",
"focus-within:ring-2 focus-within:ring-primary/20",
borderStyles[borderStyle],
borderWidths[borderWidth],
className,
)}
style={{
minHeight,
backgroundColor,
...gridBackground,
}}
>
{elements}
</div>
);
}
Update the import paths to match your project setup.
import { ArtBoardLayout } from "@/components/ui/art-board-layout";
Examples
Basic Usage
export function BasicBoard() {
return (
<ArtBoardLayout className="min-h-[300px] w-96">
<div className="rounded-lg bg-gray-500/10 p-6 backdrop-blur-xs">
<h3 className="font-semibold text-black/80">Drag me!</h3>
<p className="text-sm text-muted-foreground">Use mouse to drag</p>
</div>
</ArtBoardLayout>
);
}
Custom Styling
export function StyledBoard() {
return (
<ArtBoardLayout
borderStyle="rustic"
borderWidth="thick"
gridColor="rgba(0, 0, 255, 0.03)"
backgroundColor="#fafafa"
gridSize={40}
className="min-h-[300px]"
>
<div className="rounded-lg bg-emerald-500/10 p-6 backdrop-blur-xs">
<h3 className="font-semibold text-black/80">Custom Grid</h3>
<p className="text-sm text-muted-foreground">40px grid size</p>
</div>
</ArtBoardLayout>
);
}
Props
ArtBoardLayout
Prop | Type | Default | Description |
---|---|---|---|
gridSize | number | 20 | Size of the background grid in pixels |
borderWidth | "thin" | "medium" | "thick" | "medium" | Width of the mounting board border |
borderStyle | "modern" | "classic" | "rustic" | "modern" | Style of the mounting board border |
backgroundColor | string | "white" | Background color of the board |
gridColor | string | "rgba(0, 0, 0, 0.05)" | Color of the grid lines |
snapToGrid | boolean | true | Whether items should snap to grid when dropped |
showGridLines | boolean | true | Whether to show the background grid |
minHeight | string | "400px" | Minimum height of the layout board |
className | string | - | Additional CSS classes for styling |
children | React.ReactNode | - | Draggable content elements |