diff --git a/apps/studio/pages/project/[ref]/canvas/index.tsx b/apps/studio/pages/project/[ref]/canvas/index.tsx
new file mode 100644
index 00000000000..3e022189297
--- /dev/null
+++ b/apps/studio/pages/project/[ref]/canvas/index.tsx
@@ -0,0 +1,310 @@
+import React, { useState, useEffect, useCallback, useRef } from 'react'
+import { useRouter } from 'next/router'
+import { useSearchParams } from 'next/navigation'
+import { useParams } from 'common'
+import { motion } from 'framer-motion'
+import { Button } from 'ui'
+import { ProjectLayoutWithAuth } from 'components/layouts/ProjectLayout/ProjectLayout'
+import { SqlCard } from 'components/ui/AIAssistantPanel/SqlSnippet'
+import type { NextPageWithLayout } from 'types'
+import { GripVertical } from 'lucide-react'
+import {
+ useSnippets,
+ useSqlEditorV2StateSnapshot,
+ getSqlEditorV2StateSnapshot,
+} from 'state/sql-editor-v2'
+import { useSQLSnippetFoldersQuery } from 'data/content/sql-folders-query'
+import { useContentIdQuery } from 'data/content/content-id-query'
+
+const CanvasPage: NextPageWithLayout = () => {
+ return
+}
+CanvasPage.getLayout = (page) => {page}
+
+export default CanvasPage
+
+interface DraggedItem {
+ id: number
+ x: number
+ y: number
+ width: number
+ height: number
+ props: Record
+}
+
+interface ItemWrapperProps {
+ item: DraggedItem
+ scale: number
+ onPositionUpdate: (id: number, x: number, y: number) => void
+ onSizeUpdate: (id: number, width: number, height: number) => void
+}
+
+const ItemWrapper: React.FC = ({
+ item,
+ scale,
+ onPositionUpdate,
+ onSizeUpdate,
+}) => {
+ const { ref } = useParams()
+ const [isDragging, setIsDragging] = useState(false)
+ const [size, setSize] = useState({ width: item.width, height: item.height })
+ const lastPosition = useRef({ x: 0, y: 0 })
+ const [position, setPosition] = useState({ x: item.x, y: item.y })
+ const isItemDragging = useRef(false)
+
+ const { data: contentData } = useContentIdQuery({
+ projectRef: ref!,
+ id: item.id.toString(),
+ })
+
+ const handleMouseDown = (e: React.MouseEvent) => {
+ e.preventDefault() // Prevent text selection
+ isItemDragging.current = true
+ lastPosition.current = { x: e.clientX, y: e.clientY }
+ }
+
+ const handleMouseMove = (e: React.MouseEvent) => {
+ if (!isItemDragging.current) return
+ e.preventDefault() // Prevent text selection
+
+ const deltaX = (e.clientX - lastPosition.current.x) / scale
+ const deltaY = (e.clientY - lastPosition.current.y) / scale
+
+ setPosition((prev) => ({
+ x: prev.x + deltaX,
+ y: prev.y + deltaY,
+ }))
+
+ onPositionUpdate(item.id, position.x + deltaX, position.y + deltaY)
+ lastPosition.current = { x: e.clientX, y: e.clientY }
+ }
+
+ const handleMouseUp = () => {
+ isItemDragging.current = false
+ }
+
+ if (!contentData) return null
+
+ let formatted = contentData.content.sql
+ const propsMatch = formatted.match(/--\s*props:\s*(\{[^}]+\})/)
+ const props = propsMatch ? JSON.parse(propsMatch[1]) : {}
+ const title = props.title || 'SQL Query'
+ formatted = formatted.replace(/--\s*props:\s*\{[^}]+\}/, '').trim()
+
+ const sqlCardProps = {
+ sql: formatted,
+ title,
+ ...props,
+ }
+
+ return (
+
+
+
+
+
+
{
+ const newWidth = Math.max(200, size.width + info.delta.x / scale)
+ const newHeight = Math.max(100, size.height + info.delta.y / scale)
+ setSize({ width: newWidth, height: newHeight })
+ onSizeUpdate(item.id, newWidth, newHeight)
+ }}
+ onDragStart={(e) => {
+ e.preventDefault()
+ setIsDragging(true)
+ }}
+ onDragEnd={() => setIsDragging(false)}
+ >
+
+
+
+ )
+}
+
+const Canvas: React.FC = () => {
+ const router = useRouter()
+ const searchParams = useSearchParams()
+ const [items, setItems] = useState([])
+ const debounceTimer = useRef()
+ const [scale, setScale] = useState(1)
+ const [position, setPosition] = useState({ x: 0, y: 0 })
+ const lastPosition = useRef({ x: 0, y: 0 })
+ const isDragging = useRef(false)
+
+ useEffect(() => {
+ const queryItems = searchParams.get('items')
+ if (queryItems) {
+ try {
+ const parsedItems = JSON.parse(decodeURIComponent(queryItems))
+ const itemsWithDefaults = parsedItems.map((item: any) => ({
+ ...item,
+ x: item.x ?? 0,
+ y: item.y ?? 0,
+ width: item.width ?? 400,
+ height: item.height ?? 150,
+ }))
+ setItems(itemsWithDefaults)
+ } catch (error) {
+ console.error('Failed to parse items from URL:', error)
+ }
+ }
+ }, [searchParams])
+
+ useEffect(() => {
+ if (items.length === 0) return
+
+ const queryString = encodeURIComponent(
+ JSON.stringify(
+ items.map((item) => ({
+ id: item.id,
+ x: item.x,
+ y: item.y,
+ width: item.width,
+ height: item.height,
+ props: item.props,
+ }))
+ )
+ )
+
+ if (debounceTimer.current) {
+ clearTimeout(debounceTimer.current)
+ }
+
+ debounceTimer.current = setTimeout(() => {
+ router.replace(
+ {
+ query: { ...router.query, items: queryString },
+ },
+ undefined,
+ { shallow: true }
+ )
+ }, 500)
+
+ return () => {
+ if (debounceTimer.current) {
+ clearTimeout(debounceTimer.current)
+ }
+ }
+ }, [items, router])
+
+ const handlePositionUpdate = useCallback((id: number, x: number, y: number) => {
+ setItems((prev) => prev.map((item) => (item.id === id ? { ...item, x, y } : item)))
+ }, [])
+
+ const handleSizeUpdate = useCallback((id: number, width: number, height: number) => {
+ setItems((prev) => prev.map((item) => (item.id === id ? { ...item, width, height } : item)))
+ }, [])
+
+ const handleMouseDown = (e: React.MouseEvent) => {
+ if ((e.target as HTMLElement).closest('.border')) return
+ e.preventDefault()
+ isDragging.current = true
+ lastPosition.current = { x: e.clientX, y: e.clientY }
+ }
+
+ const handleMouseMove = (e: React.MouseEvent) => {
+ if (!isDragging.current) return
+ e.preventDefault()
+
+ const deltaX = e.clientX - lastPosition.current.x
+ const deltaY = e.clientY - lastPosition.current.y
+
+ setPosition((prev) => ({
+ x: prev.x + deltaX,
+ y: prev.y + deltaY,
+ }))
+
+ lastPosition.current = { x: e.clientX, y: e.clientY }
+ }
+
+ const handleMouseUp = () => {
+ isDragging.current = false
+ }
+
+ const handleZoomIn = () => {
+ setScale((prev) => Math.min(prev + 0.1, 4))
+ }
+
+ const handleZoomOut = () => {
+ setScale((prev) => Math.max(prev - 0.1, 0.1))
+ }
+
+ return (
+
+
+
+
+
+
+ {items.map((item) => (
+
+ ))}
+
+
+ )
+}
+
+function Component() {
+ return (
+
+
+
+ )
+}