use-transformable
Version:
A React library for creating transformable, draggable, resizable, and rotatable elements with multi-selection support
1 lines • 93.2 kB
Source Map (JSON)
{"version":3,"file":"index.esm","sources":["../node_modules/react/cjs/react-jsx-runtime.production.js","../node_modules/react/cjs/react-jsx-runtime.development.js","../node_modules/react/jsx-runtime.js","../src/themes/defaultTheme.ts","../src/contexts/TransformableContext.tsx","../src/components/TransformableItem.tsx","../src/components/SelectionBox.tsx","../src/hooks/useTransformable.ts","../src/hooks/useSelectionTool.ts"],"sourcesContent":["/**\n * @license React\n * react-jsx-runtime.production.js\n *\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n\"use strict\";\nvar REACT_ELEMENT_TYPE = Symbol.for(\"react.transitional.element\"),\n REACT_FRAGMENT_TYPE = Symbol.for(\"react.fragment\");\nfunction jsxProd(type, config, maybeKey) {\n var key = null;\n void 0 !== maybeKey && (key = \"\" + maybeKey);\n void 0 !== config.key && (key = \"\" + config.key);\n if (\"key\" in config) {\n maybeKey = {};\n for (var propName in config)\n \"key\" !== propName && (maybeKey[propName] = config[propName]);\n } else maybeKey = config;\n config = maybeKey.ref;\n return {\n $$typeof: REACT_ELEMENT_TYPE,\n type: type,\n key: key,\n ref: void 0 !== config ? config : null,\n props: maybeKey\n };\n}\nexports.Fragment = REACT_FRAGMENT_TYPE;\nexports.jsx = jsxProd;\nexports.jsxs = jsxProd;\n","/**\n * @license React\n * react-jsx-runtime.development.js\n *\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n\"use strict\";\n\"production\" !== process.env.NODE_ENV &&\n (function () {\n function getComponentNameFromType(type) {\n if (null == type) return null;\n if (\"function\" === typeof type)\n return type.$$typeof === REACT_CLIENT_REFERENCE\n ? null\n : type.displayName || type.name || null;\n if (\"string\" === typeof type) return type;\n switch (type) {\n case REACT_FRAGMENT_TYPE:\n return \"Fragment\";\n case REACT_PROFILER_TYPE:\n return \"Profiler\";\n case REACT_STRICT_MODE_TYPE:\n return \"StrictMode\";\n case REACT_SUSPENSE_TYPE:\n return \"Suspense\";\n case REACT_SUSPENSE_LIST_TYPE:\n return \"SuspenseList\";\n case REACT_ACTIVITY_TYPE:\n return \"Activity\";\n }\n if (\"object\" === typeof type)\n switch (\n (\"number\" === typeof type.tag &&\n console.error(\n \"Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue.\"\n ),\n type.$$typeof)\n ) {\n case REACT_PORTAL_TYPE:\n return \"Portal\";\n case REACT_CONTEXT_TYPE:\n return (type.displayName || \"Context\") + \".Provider\";\n case REACT_CONSUMER_TYPE:\n return (type._context.displayName || \"Context\") + \".Consumer\";\n case REACT_FORWARD_REF_TYPE:\n var innerType = type.render;\n type = type.displayName;\n type ||\n ((type = innerType.displayName || innerType.name || \"\"),\n (type = \"\" !== type ? \"ForwardRef(\" + type + \")\" : \"ForwardRef\"));\n return type;\n case REACT_MEMO_TYPE:\n return (\n (innerType = type.displayName || null),\n null !== innerType\n ? innerType\n : getComponentNameFromType(type.type) || \"Memo\"\n );\n case REACT_LAZY_TYPE:\n innerType = type._payload;\n type = type._init;\n try {\n return getComponentNameFromType(type(innerType));\n } catch (x) {}\n }\n return null;\n }\n function testStringCoercion(value) {\n return \"\" + value;\n }\n function checkKeyStringCoercion(value) {\n try {\n testStringCoercion(value);\n var JSCompiler_inline_result = !1;\n } catch (e) {\n JSCompiler_inline_result = !0;\n }\n if (JSCompiler_inline_result) {\n JSCompiler_inline_result = console;\n var JSCompiler_temp_const = JSCompiler_inline_result.error;\n var JSCompiler_inline_result$jscomp$0 =\n (\"function\" === typeof Symbol &&\n Symbol.toStringTag &&\n value[Symbol.toStringTag]) ||\n value.constructor.name ||\n \"Object\";\n JSCompiler_temp_const.call(\n JSCompiler_inline_result,\n \"The provided key is an unsupported type %s. This value must be coerced to a string before using it here.\",\n JSCompiler_inline_result$jscomp$0\n );\n return testStringCoercion(value);\n }\n }\n function getTaskName(type) {\n if (type === REACT_FRAGMENT_TYPE) return \"<>\";\n if (\n \"object\" === typeof type &&\n null !== type &&\n type.$$typeof === REACT_LAZY_TYPE\n )\n return \"<...>\";\n try {\n var name = getComponentNameFromType(type);\n return name ? \"<\" + name + \">\" : \"<...>\";\n } catch (x) {\n return \"<...>\";\n }\n }\n function getOwner() {\n var dispatcher = ReactSharedInternals.A;\n return null === dispatcher ? null : dispatcher.getOwner();\n }\n function UnknownOwner() {\n return Error(\"react-stack-top-frame\");\n }\n function hasValidKey(config) {\n if (hasOwnProperty.call(config, \"key\")) {\n var getter = Object.getOwnPropertyDescriptor(config, \"key\").get;\n if (getter && getter.isReactWarning) return !1;\n }\n return void 0 !== config.key;\n }\n function defineKeyPropWarningGetter(props, displayName) {\n function warnAboutAccessingKey() {\n specialPropKeyWarningShown ||\n ((specialPropKeyWarningShown = !0),\n console.error(\n \"%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://react.dev/link/special-props)\",\n displayName\n ));\n }\n warnAboutAccessingKey.isReactWarning = !0;\n Object.defineProperty(props, \"key\", {\n get: warnAboutAccessingKey,\n configurable: !0\n });\n }\n function elementRefGetterWithDeprecationWarning() {\n var componentName = getComponentNameFromType(this.type);\n didWarnAboutElementRef[componentName] ||\n ((didWarnAboutElementRef[componentName] = !0),\n console.error(\n \"Accessing element.ref was removed in React 19. ref is now a regular prop. It will be removed from the JSX Element type in a future release.\"\n ));\n componentName = this.props.ref;\n return void 0 !== componentName ? componentName : null;\n }\n function ReactElement(\n type,\n key,\n self,\n source,\n owner,\n props,\n debugStack,\n debugTask\n ) {\n self = props.ref;\n type = {\n $$typeof: REACT_ELEMENT_TYPE,\n type: type,\n key: key,\n props: props,\n _owner: owner\n };\n null !== (void 0 !== self ? self : null)\n ? Object.defineProperty(type, \"ref\", {\n enumerable: !1,\n get: elementRefGetterWithDeprecationWarning\n })\n : Object.defineProperty(type, \"ref\", { enumerable: !1, value: null });\n type._store = {};\n Object.defineProperty(type._store, \"validated\", {\n configurable: !1,\n enumerable: !1,\n writable: !0,\n value: 0\n });\n Object.defineProperty(type, \"_debugInfo\", {\n configurable: !1,\n enumerable: !1,\n writable: !0,\n value: null\n });\n Object.defineProperty(type, \"_debugStack\", {\n configurable: !1,\n enumerable: !1,\n writable: !0,\n value: debugStack\n });\n Object.defineProperty(type, \"_debugTask\", {\n configurable: !1,\n enumerable: !1,\n writable: !0,\n value: debugTask\n });\n Object.freeze && (Object.freeze(type.props), Object.freeze(type));\n return type;\n }\n function jsxDEVImpl(\n type,\n config,\n maybeKey,\n isStaticChildren,\n source,\n self,\n debugStack,\n debugTask\n ) {\n var children = config.children;\n if (void 0 !== children)\n if (isStaticChildren)\n if (isArrayImpl(children)) {\n for (\n isStaticChildren = 0;\n isStaticChildren < children.length;\n isStaticChildren++\n )\n validateChildKeys(children[isStaticChildren]);\n Object.freeze && Object.freeze(children);\n } else\n console.error(\n \"React.jsx: Static children should always be an array. You are likely explicitly calling React.jsxs or React.jsxDEV. Use the Babel transform instead.\"\n );\n else validateChildKeys(children);\n if (hasOwnProperty.call(config, \"key\")) {\n children = getComponentNameFromType(type);\n var keys = Object.keys(config).filter(function (k) {\n return \"key\" !== k;\n });\n isStaticChildren =\n 0 < keys.length\n ? \"{key: someKey, \" + keys.join(\": ..., \") + \": ...}\"\n : \"{key: someKey}\";\n didWarnAboutKeySpread[children + isStaticChildren] ||\n ((keys =\n 0 < keys.length ? \"{\" + keys.join(\": ..., \") + \": ...}\" : \"{}\"),\n console.error(\n 'A props object containing a \"key\" prop is being spread into JSX:\\n let props = %s;\\n <%s {...props} />\\nReact keys must be passed directly to JSX without using spread:\\n let props = %s;\\n <%s key={someKey} {...props} />',\n isStaticChildren,\n children,\n keys,\n children\n ),\n (didWarnAboutKeySpread[children + isStaticChildren] = !0));\n }\n children = null;\n void 0 !== maybeKey &&\n (checkKeyStringCoercion(maybeKey), (children = \"\" + maybeKey));\n hasValidKey(config) &&\n (checkKeyStringCoercion(config.key), (children = \"\" + config.key));\n if (\"key\" in config) {\n maybeKey = {};\n for (var propName in config)\n \"key\" !== propName && (maybeKey[propName] = config[propName]);\n } else maybeKey = config;\n children &&\n defineKeyPropWarningGetter(\n maybeKey,\n \"function\" === typeof type\n ? type.displayName || type.name || \"Unknown\"\n : type\n );\n return ReactElement(\n type,\n children,\n self,\n source,\n getOwner(),\n maybeKey,\n debugStack,\n debugTask\n );\n }\n function validateChildKeys(node) {\n \"object\" === typeof node &&\n null !== node &&\n node.$$typeof === REACT_ELEMENT_TYPE &&\n node._store &&\n (node._store.validated = 1);\n }\n var React = require(\"react\"),\n REACT_ELEMENT_TYPE = Symbol.for(\"react.transitional.element\"),\n REACT_PORTAL_TYPE = Symbol.for(\"react.portal\"),\n REACT_FRAGMENT_TYPE = Symbol.for(\"react.fragment\"),\n REACT_STRICT_MODE_TYPE = Symbol.for(\"react.strict_mode\"),\n REACT_PROFILER_TYPE = Symbol.for(\"react.profiler\");\n Symbol.for(\"react.provider\");\n var REACT_CONSUMER_TYPE = Symbol.for(\"react.consumer\"),\n REACT_CONTEXT_TYPE = Symbol.for(\"react.context\"),\n REACT_FORWARD_REF_TYPE = Symbol.for(\"react.forward_ref\"),\n REACT_SUSPENSE_TYPE = Symbol.for(\"react.suspense\"),\n REACT_SUSPENSE_LIST_TYPE = Symbol.for(\"react.suspense_list\"),\n REACT_MEMO_TYPE = Symbol.for(\"react.memo\"),\n REACT_LAZY_TYPE = Symbol.for(\"react.lazy\"),\n REACT_ACTIVITY_TYPE = Symbol.for(\"react.activity\"),\n REACT_CLIENT_REFERENCE = Symbol.for(\"react.client.reference\"),\n ReactSharedInternals =\n React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,\n hasOwnProperty = Object.prototype.hasOwnProperty,\n isArrayImpl = Array.isArray,\n createTask = console.createTask\n ? console.createTask\n : function () {\n return null;\n };\n React = {\n react_stack_bottom_frame: function (callStackForError) {\n return callStackForError();\n }\n };\n var specialPropKeyWarningShown;\n var didWarnAboutElementRef = {};\n var unknownOwnerDebugStack = React.react_stack_bottom_frame.bind(\n React,\n UnknownOwner\n )();\n var unknownOwnerDebugTask = createTask(getTaskName(UnknownOwner));\n var didWarnAboutKeySpread = {};\n exports.Fragment = REACT_FRAGMENT_TYPE;\n exports.jsx = function (type, config, maybeKey, source, self) {\n var trackActualOwner =\n 1e4 > ReactSharedInternals.recentlyCreatedOwnerStacks++;\n return jsxDEVImpl(\n type,\n config,\n maybeKey,\n !1,\n source,\n self,\n trackActualOwner\n ? Error(\"react-stack-top-frame\")\n : unknownOwnerDebugStack,\n trackActualOwner ? createTask(getTaskName(type)) : unknownOwnerDebugTask\n );\n };\n exports.jsxs = function (type, config, maybeKey, source, self) {\n var trackActualOwner =\n 1e4 > ReactSharedInternals.recentlyCreatedOwnerStacks++;\n return jsxDEVImpl(\n type,\n config,\n maybeKey,\n !0,\n source,\n self,\n trackActualOwner\n ? Error(\"react-stack-top-frame\")\n : unknownOwnerDebugStack,\n trackActualOwner ? createTask(getTaskName(type)) : unknownOwnerDebugTask\n );\n };\n })();\n","'use strict';\n\nif (process.env.NODE_ENV === 'production') {\n module.exports = require('./cjs/react-jsx-runtime.production.js');\n} else {\n module.exports = require('./cjs/react-jsx-runtime.development.js');\n}\n","import type { TransformableTheme } from '../contexts/TransformableContext';\n\n// Default theme with original styling\nexport const defaultTheme: TransformableTheme = {\n element: {\n outline: '1px solid #007bff',\n borderRadius: '0px',\n backgroundColor: '#007bff20',\n },\n resizeHandlers: {\n base: {\n backgroundColor: 'white',\n outline: '1px solid #007bff',\n borderRadius: 0,\n width: 8,\n height: 8,\n },\n topLeft: {},\n top: {},\n topRight: {},\n right: {},\n bottomRight: {},\n bottom: {},\n bottomLeft: {},\n left: {},\n },\n rotationHandler: {\n base: {\n backgroundColor: 'transparent',\n border: 'none',\n borderRadius: 0,\n width: 8,\n height: 8,\n },\n topLeft: {},\n topRight: {},\n bottomRight: {},\n bottomLeft: {},\n },\n origin: {\n position: 'absolute',\n width: '5px',\n height: '5px',\n backgroundColor: '#007bff',\n outline: '1px solid #fff',\n borderRadius: '50%',\n pointerEvents: 'none',\n zIndex: 10001,\n },\n};\n","import React, { createContext, useContext, useCallback } from 'react';\nimport type { Transformable, Transformation } from '../hooks/useTransformable';\nimport { defaultTheme } from '../themes/defaultTheme';\n\nexport interface TransformableTheme {\n element?: React.CSSProperties;\n resizeHandlers?: {\n base?: React.CSSProperties;\n [direction: string]: React.CSSProperties | undefined;\n };\n rotationHandler?: {\n base?: React.CSSProperties;\n [direction: string]: React.CSSProperties | undefined;\n };\n origin?: React.CSSProperties;\n}\n\ninterface TransformableContextType {\n // Selection state\n selected: Set<string>;\n setSelected: (ids: Set<string>) => void;\n \n // Grid configuration\n gridSize: number;\n snapToGrid: boolean;\n \n // Theme configuration\n theme: TransformableTheme;\n \n // Origin display\n showOrigin?: boolean;\n \n // Elements data\n elements: Transformable[];\n \n // Transformation callbacks\n onDragEnd: (transformation: Transformation) => void;\n onMultiDragEnd: (transformations: Transformation[]) => void;\n onMultiDragMove: (deltaX: number, deltaY: number) => void;\n onMultiResizeMove: (deltaX: number, deltaY: number, deltaWidth: number, deltaHeight: number) => void;\n onMultiResizeEnd: (transformations: Transformation[]) => void;\n onMultiRotateMove: (deltaRotation: number) => void;\n onMultiRotateEnd: (transformations: Transformation[]) => void;\n onResizeEnd: (transformation: Transformation) => void;\n onRotateEnd: (transformation: Transformation) => void;\n onSelect: (elementId: string, isMultiSelect: boolean) => void;\n \n // Utility functions\n snapValue: (value: number) => number;\n}\n\nconst TransformableContext = createContext<TransformableContextType | null>(null);\n\ninterface TransformableProviderProps {\n children: React.ReactNode;\n selected: Set<string>;\n setSelected: (ids: Set<string>) => void;\n gridSize: number;\n snapToGrid: boolean;\n elements: Transformable[];\n handleUpdate: (id: string, updates: Partial<Transformable>) => void;\n theme?: TransformableTheme;\n showOrigin?: boolean;\n}\n\nexport const TransformableProvider: React.FC<TransformableProviderProps> = ({\n children,\n selected,\n setSelected,\n gridSize,\n snapToGrid,\n elements,\n handleUpdate,\n theme,\n showOrigin,\n}) => {\n const snapValue = useCallback((value: number) => {\n return snapToGrid ? Math.round(value / gridSize) * gridSize : value;\n }, [snapToGrid, gridSize]);\n\n // Merge custom theme with default theme\n const mergedTheme = useCallback(() => {\n if (!theme) return defaultTheme;\n \n return {\n element: {\n ...defaultTheme.element,\n ...theme.element,\n },\n resizeHandlers: {\n ...defaultTheme.resizeHandlers,\n ...theme.resizeHandlers,\n },\n rotationHandler: {\n ...defaultTheme.rotationHandler,\n ...theme.rotationHandler,\n },\n origin: {\n ...defaultTheme.origin,\n ...theme.origin,\n },\n };\n }, [theme])();\n\n const onDragEnd = useCallback((transformation: Transformation) => {\n handleUpdate(transformation.id, { x: transformation.x!, y: transformation.y! });\n }, [handleUpdate]);\n\n const onMultiDragEnd = useCallback((transformations: Transformation[]) => {\n transformations.forEach(transformation => {\n if (transformation.deltaX !== undefined && transformation.deltaY !== undefined) {\n const element = elements.find(el => el.id === transformation.id);\n if (element) {\n const newX = snapValue(element.x + transformation.deltaX);\n const newY = snapValue(element.y + transformation.deltaY);\n handleUpdate(transformation.id, { x: newX, y: newY });\n }\n }\n });\n }, [elements, snapValue, handleUpdate]);\n\n const onMultiDragMove = useCallback((deltaX: number, deltaY: number) => {\n // Update all selected elements in real-time during multi-drag\n selected.forEach(id => {\n const element = elements.find(el => el.id === id);\n if (element) {\n const newX = snapValue(element.x + deltaX);\n const newY = snapValue(element.y + deltaY);\n handleUpdate(id, { x: newX, y: newY });\n }\n });\n }, [selected, elements, snapValue, handleUpdate]);\n\n const onMultiResizeMove = useCallback((deltaX: number, deltaY: number, deltaWidth: number, deltaHeight: number) => {\n // Update all selected elements in real-time during multi-resize\n selected.forEach(id => {\n const element = elements.find(el => el.id === id);\n if (element) {\n const newX = snapValue(element.x + deltaX);\n const newY = snapValue(element.y + deltaY);\n const newWidth = Math.max(50, snapValue(element.width + deltaWidth));\n const newHeight = Math.max(50, snapValue(element.height + deltaHeight));\n handleUpdate(id, { x: newX, y: newY, width: newWidth, height: newHeight });\n }\n });\n }, [selected, elements, snapValue, handleUpdate]);\n\n const onMultiResizeEnd = useCallback((transformations: Transformation[]) => {\n transformations.forEach(transformation => {\n if (transformation.deltaX !== undefined && transformation.deltaY !== undefined && \n transformation.deltaWidth !== undefined && transformation.deltaHeight !== undefined) {\n const element = elements.find(el => el.id === transformation.id);\n if (element) {\n const newX = snapValue(element.x + transformation.deltaX);\n const newY = snapValue(element.y + transformation.deltaY);\n const newWidth = Math.max(50, snapValue(element.width + transformation.deltaWidth));\n const newHeight = Math.max(50, snapValue(element.height + transformation.deltaHeight));\n handleUpdate(transformation.id, { x: newX, y: newY, width: newWidth, height: newHeight });\n }\n }\n });\n }, [elements, snapValue, handleUpdate]);\n\n const onMultiRotateMove = useCallback((deltaRotation: number) => {\n // Update all selected elements in real-time during multi-rotate\n selected.forEach(id => {\n const element = elements.find(el => el.id === id);\n if (element) {\n const newRotation = element.rotation + deltaRotation;\n handleUpdate(id, { rotation: newRotation });\n }\n });\n }, [selected, elements, handleUpdate]);\n\n const onMultiRotateEnd = useCallback((transformations: Transformation[]) => {\n transformations.forEach(transformation => {\n if (transformation.deltaRotation !== undefined) {\n const element = elements.find(el => el.id === transformation.id);\n if (element) {\n const newRotation = element.rotation + transformation.deltaRotation;\n handleUpdate(transformation.id, { rotation: newRotation });\n }\n }\n });\n }, [elements, handleUpdate]);\n\n const onResizeEnd = useCallback((transformation: Transformation) => {\n handleUpdate(transformation.id, { \n x: transformation.x!, \n y: transformation.y!, \n width: transformation.width!, \n height: transformation.height! \n });\n }, [handleUpdate]);\n\n const onRotateEnd = useCallback((transformation: Transformation) => {\n handleUpdate(transformation.id, { rotation: transformation.rotation! });\n }, [handleUpdate]);\n\n const onSelect = useCallback((elementId: string, isMultiSelect: boolean) => {\n if (isMultiSelect) {\n const newSelection = new Set(selected);\n if (newSelection.has(elementId)) {\n newSelection.delete(elementId);\n } else {\n newSelection.add(elementId);\n }\n setSelected(newSelection);\n } else {\n if (!selected.has(elementId)) {\n setSelected(new Set([elementId]));\n }\n }\n }, [selected, setSelected]);\n\n const contextValue: TransformableContextType = {\n selected,\n setSelected,\n gridSize,\n snapToGrid,\n elements,\n onDragEnd,\n onMultiDragEnd,\n onMultiDragMove,\n onMultiResizeMove,\n onMultiResizeEnd,\n onMultiRotateMove,\n onMultiRotateEnd,\n onResizeEnd,\n onRotateEnd,\n onSelect,\n snapValue,\n theme: mergedTheme,\n showOrigin,\n };\n\n return (\n <TransformableContext.Provider value={contextValue}>\n {children}\n </TransformableContext.Provider>\n );\n};\n\nexport const useTransformable = (): TransformableContextType => {\n const context = useContext(TransformableContext);\n if (!context) {\n throw new Error('useTransformable must be used within a TransformableProvider');\n }\n return context;\n};\n","import React, { useCallback, useRef, useEffect } from 'react';\nimport { useTransformable } from '../contexts/TransformableContext';\n\ninterface TransformableItemProps {\n id: string;\n children?: React.ReactNode;\n}\n\nexport const TransformableItem: React.FC<TransformableItemProps> = ({\n id,\n children,\n}) => {\n const {\n elements,\n selected,\n onDragEnd,\n onMultiDragEnd,\n onMultiDragMove,\n onMultiResizeMove,\n onMultiRotateMove,\n onResizeEnd,\n onRotateEnd,\n onSelect,\n snapValue,\n theme,\n showOrigin,\n } = useTransformable();\n\n // Get element data from context using the id\n const element = elements.find((el: any) => el.id === id);\n if (!element) {\n console.warn(`TransformableItem: Element with id \"${id}\" not found`);\n return null;\n }\n\n const isSelected = selected.has(element.id);\n\n // Helper function to merge theme styles with defaults\n const getThemeStyle = (baseStyle: React.CSSProperties, themeStyle?: React.CSSProperties) => {\n return themeStyle ? { ...baseStyle, ...themeStyle } : baseStyle;\n };\n\n // Helper function to get resize handler style\n const getResizeHandlerStyle = (direction: string, baseStyle: React.CSSProperties) => {\n const baseHandlerStyle = theme.resizeHandlers!.base || {};\n const specificHandlerStyle = theme.resizeHandlers![direction] || {};\n return { ...baseStyle, ...baseHandlerStyle, ...specificHandlerStyle };\n };\n\n // Helper function to get rotation handler style\n const getRotationHandlerStyle = (direction: string, baseStyle: React.CSSProperties) => {\n const baseHandlerStyle = theme.rotationHandler!.base || {};\n const specificHandlerStyle = theme.rotationHandler![direction] || {};\n return { ...baseStyle, ...baseHandlerStyle, ...specificHandlerStyle };\n };\n\n const elementRef = useRef<HTMLDivElement>(null);\n const selectionOutlineRef = useRef<HTMLDivElement>(null);\n const dragStateRef = useRef<{ isDragging: boolean; startX: number; startY: number; offsetX: number; offsetY: number; isMultiDrag: boolean; isDraggingThis: boolean }>({ isDragging: false, startX: 0, startY: 0, offsetX: 0, offsetY: 0, isMultiDrag: false, isDraggingThis: false });\n const resizeStateRef = useRef<{ isResizing: boolean; handle: string; startWidth: number; startHeight: number; startX: number; startY: number }>({ isResizing: false, handle: '', startWidth: 0, startHeight: 0, startX: 0, startY: 0 });\n const rotateStateRef = useRef<{ isRotating: boolean; startAngle: number; centerX: number; centerY: number }>({ isRotating: false, startAngle: 0, centerX: 0, centerY: 0 });\n const lastMousePosRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });\n\n const getCanvasCoordinates = useCallback((event: React.MouseEvent | MouseEvent) => {\n const canvasElement = document.querySelector('[data-canvas]') as HTMLElement;\n if (!canvasElement) return { x: 0, y: 0 };\n const rect = canvasElement.getBoundingClientRect();\n return {\n x: event.clientX - rect.left,\n y: event.clientY - rect.top,\n };\n }, []);\n\n const handleMouseDown = useCallback((event: React.MouseEvent) => {\n if (event.button !== 0) return;\n \n const coords = getCanvasCoordinates(event);\n const target = event.target as HTMLElement;\n \n // Check if clicking on a resize handle\n if (target.classList.contains('resize-handle')) {\n const elementId = target.getAttribute('data-element-id');\n const handle = target.getAttribute('data-handle');\n if (elementId && handle) {\n resizeStateRef.current = {\n isResizing: true,\n handle,\n startWidth: element.width,\n startHeight: element.height,\n startX: coords.x,\n startY: coords.y,\n };\n onSelect(element.id, false);\n }\n return;\n }\n \n // Check if clicking on rotate handle\n if (target.classList.contains('rotate-handle')) {\n const elementId = target.getAttribute('data-element-id');\n const handle = target.getAttribute('data-handle');\n if (elementId && handle) {\n const centerX = element.x + element.origin[0] * element.width;\n const centerY = element.y + element.origin[1] * element.height;\n const rawAngle = Math.atan2(coords.y - centerY, coords.x - centerX) * 180 / Math.PI;\n const startAngle = rawAngle - element.rotation;\n rotateStateRef.current = {\n isRotating: true,\n startAngle,\n centerX,\n centerY,\n };\n onSelect(element.id, false);\n }\n return;\n }\n \n // Check if clicking on the element itself\n if (target.classList.contains('element')) {\n const elementId = target.getAttribute('data-element-id');\n if (elementId) {\n onSelect(elementId, event.shiftKey);\n \n // Check if this is a multi-drag (dragging a selected element)\n const isMultiDrag = selected.size > 1 && selected.has(element.id);\n const isDraggingThis = true; // This element is the one being dragged\n \n dragStateRef.current = {\n isDragging: true,\n startX: coords.x,\n startY: coords.y,\n offsetX: coords.x - element.x,\n offsetY: coords.y - element.y,\n isMultiDrag,\n isDraggingThis,\n };\n }\n return;\n }\n }, [element, selected, getCanvasCoordinates, onSelect]);\n\n const handleResizeMouseDown = useCallback((event: React.MouseEvent, handle: string) => {\n if (event.button !== 0) return;\n event.stopPropagation();\n \n const coords = getCanvasCoordinates(event);\n \n resizeStateRef.current = {\n isResizing: true,\n handle,\n startWidth: element.width,\n startHeight: element.height,\n startX: coords.x,\n startY: coords.y,\n };\n onSelect(element.id, false);\n }, [element, selected, getCanvasCoordinates, onSelect]);\n\n const handleRotateMouseDown = useCallback((event: React.MouseEvent) => {\n if (event.button !== 0) return;\n event.stopPropagation();\n \n const coords = getCanvasCoordinates(event);\n const centerX = element.x + element.origin[0] * element.width;\n const centerY = element.y + element.origin[1] * element.height;\n const rawAngle = Math.atan2(coords.y - centerY, coords.x - centerX) * 180 / Math.PI;\n const startAngle = rawAngle - element.rotation;\n\n rotateStateRef.current = {\n isRotating: true,\n startAngle,\n centerX,\n centerY,\n };\n onSelect(element.id, false);\n }, [element, selected, getCanvasCoordinates, onSelect]);\n\n const handleMouseMove = useCallback((event: MouseEvent) => {\n const coords = getCanvasCoordinates(event);\n lastMousePosRef.current = coords;\n \n if (dragStateRef.current.isDragging) {\n const newX = snapValue(coords.x - dragStateRef.current.offsetX);\n const newY = snapValue(coords.y - dragStateRef.current.offsetY);\n \n if (dragStateRef.current.isMultiDrag && dragStateRef.current.isDraggingThis) {\n // Calculate delta for multi-drag and notify parent\n const deltaX = newX - element.x;\n const deltaY = newY - element.y;\n onMultiDragMove(deltaX, deltaY);\n }\n \n // Apply transformation directly for real-time feedback\n if (elementRef.current) {\n elementRef.current.style.left = `${newX}px`;\n elementRef.current.style.top = `${newY}px`;\n }\n \n // Update selection outline and handles\n if (selectionOutlineRef.current) {\n selectionOutlineRef.current.style.left = `${newX}px`;\n selectionOutlineRef.current.style.top = `${newY}px`;\n }\n \n // Update origin indicator position\n if (showOrigin) {\n const originElement = document.querySelector(`[data-origin-id=\"${element.id}\"]`) as HTMLElement;\n if (originElement) {\n const originX = newX + element.origin[0] * element.width - 3;\n const originY = newY + element.origin[1] * element.height - 3;\n originElement.style.left = `${originX}px`;\n originElement.style.top = `${originY}px`;\n }\n }\n }\n \n if (resizeStateRef.current.isResizing) {\n const handleDeltaX = coords.x - resizeStateRef.current.startX;\n const handleDeltaY = coords.y - resizeStateRef.current.startY;\n \n let newWidth = element.width;\n let newHeight = element.height;\n let newX = element.x;\n let newY = element.y;\n \n if (resizeStateRef.current.handle.includes('right') || resizeStateRef.current.handle === 'right') {\n newWidth = Math.max(50, snapValue(resizeStateRef.current.startWidth + handleDeltaX));\n }\n if (resizeStateRef.current.handle.includes('left') || resizeStateRef.current.handle === 'left') {\n newWidth = Math.max(50, snapValue(resizeStateRef.current.startWidth - handleDeltaX));\n newX = snapValue(element.x + handleDeltaX);\n }\n if (resizeStateRef.current.handle.includes('bottom') || resizeStateRef.current.handle === 'bottom') {\n newHeight = Math.max(50, snapValue(resizeStateRef.current.startHeight + handleDeltaY));\n }\n if (resizeStateRef.current.handle.includes('top') || resizeStateRef.current.handle === 'top') {\n newHeight = Math.max(50, snapValue(resizeStateRef.current.startHeight - handleDeltaY));\n newY = snapValue(element.y + handleDeltaY);\n }\n \n // Check if this is a multi-resize\n const isMultiResize = selected.size > 1 && selected.has(element.id);\n \n if (isMultiResize) {\n // Calculate deltas for multi-resize and notify parent\n const deltaWidth = newWidth - element.width;\n const deltaHeight = newHeight - element.height;\n const deltaX = newX - element.x;\n const deltaY = newY - element.y;\n \n // Call the multi-resize callback\n onMultiResizeMove(deltaX, deltaY, deltaWidth, deltaHeight);\n }\n \n // Apply transformation directly for real-time feedback\n if (elementRef.current) {\n elementRef.current.style.left = `${newX}px`;\n elementRef.current.style.top = `${newY}px`;\n elementRef.current.style.width = `${newWidth}px`;\n elementRef.current.style.height = `${newHeight}px`;\n }\n \n // Update selection outline and handles\n if (selectionOutlineRef.current) {\n selectionOutlineRef.current.style.left = `${newX}px`;\n selectionOutlineRef.current.style.top = `${newY}px`;\n selectionOutlineRef.current.style.width = `${newWidth}px`;\n selectionOutlineRef.current.style.height = `${newHeight}px`;\n }\n \n // Update origin indicator position\n if (showOrigin) {\n const originElement = document.querySelector(`[data-origin-id=\"${element.id}\"]`) as HTMLElement;\n if (originElement) {\n const originX = newX + element.origin[0] * newWidth - 3;\n const originY = newY + element.origin[1] * newHeight - 3;\n originElement.style.left = `${originX}px`;\n originElement.style.top = `${originY}px`;\n }\n }\n }\n \n if (rotateStateRef.current.isRotating) {\n const deltaX = coords.x - rotateStateRef.current.centerX;\n const deltaY = coords.y - rotateStateRef.current.centerY;\n const currentAngle = Math.atan2(deltaY, deltaX) * 180 / Math.PI;\n const newRotation = currentAngle - rotateStateRef.current.startAngle;\n \n // Check if this is a multi-rotate\n const isMultiRotate = selected.size > 1 && selected.has(element.id);\n \n if (isMultiRotate) {\n // Calculate delta rotation for multi-rotate\n const deltaRotation = newRotation - element.rotation;\n \n // Call the multi-rotate callback\n onMultiRotateMove(deltaRotation);\n }\n \n // Apply transformation directly for real-time feedback\n if (elementRef.current) {\n elementRef.current.style.transform = `rotate(${newRotation}deg)`;\n }\n \n // Update selection outline and handles rotation\n if (selectionOutlineRef.current) {\n selectionOutlineRef.current.style.transform = `rotate(${newRotation}deg)`;\n }\n \n // Update origin indicator position (don't rotate it)\n if (showOrigin) {\n const originElement = document.querySelector(`[data-origin-id=\"${element.id}\"]`) as HTMLElement;\n if (originElement) {\n // Keep the origin indicator at the same position, don't rotate it\n const originX = element.x + element.origin[0] * element.width - 3;\n const originY = element.y + element.origin[1] * element.height - 3;\n originElement.style.left = `${originX}px`;\n originElement.style.top = `${originY}px`;\n originElement.style.transform = 'none';\n }\n }\n }\n }, [element, getCanvasCoordinates, snapValue, onMultiDragMove]);\n\n // Update origin indicator when element properties change (e.g., during multi-transformations)\n useEffect(() => {\n if (showOrigin) {\n const originElement = document.querySelector(`[data-origin-id=\"${element.id}\"]`) as HTMLElement;\n if (originElement) {\n const originX = element.x + element.origin[0] * element.width - 3;\n const originY = element.y + element.origin[1] * element.height - 3;\n originElement.style.left = `${originX}px`;\n originElement.style.top = `${originY}px`;\n // Don't rotate the origin indicator - it should stay fixed at the origin point\n originElement.style.transform = 'none';\n originElement.style.transformOrigin = 'center';\n }\n }\n }, [element.x, element.y, element.width, element.height, element.rotation, element.origin, showOrigin]);\n\n const handleMouseUp = useCallback(() => {\n if (dragStateRef.current.isDragging) {\n const coords = lastMousePosRef.current;\n const newX = snapValue(coords.x - dragStateRef.current.offsetX);\n const newY = snapValue(coords.y - dragStateRef.current.offsetY);\n \n if (dragStateRef.current.isMultiDrag) {\n // Calculate delta for multi-drag\n const deltaX = newX - element.x;\n const deltaY = newY - element.y;\n \n // Create transformations for all selected elements\n const transformations: any[] = [];\n selected.forEach(id => {\n transformations.push({\n id,\n x: undefined, // Will be calculated by parent\n y: undefined, // Will be calculated by parent\n deltaX,\n deltaY,\n });\n });\n \n onMultiDragEnd(transformations);\n } else {\n // Single element drag\n onDragEnd({\n id: element.id,\n x: newX,\n y: newY,\n });\n }\n \n // Reset drag state\n dragStateRef.current = { isDragging: false, startX: 0, startY: 0, offsetX: 0, offsetY: 0, isMultiDrag: false, isDraggingThis: false };\n }\n \n if (resizeStateRef.current.isResizing) {\n const coords = lastMousePosRef.current;\n const handleDeltaX = coords.x - resizeStateRef.current.startX;\n const handleDeltaY = coords.y - resizeStateRef.current.startY;\n \n let newWidth = element.width;\n let newHeight = element.height;\n let newX = element.x;\n let newY = element.y;\n \n if (resizeStateRef.current.handle.includes('right') || resizeStateRef.current.handle === 'right') {\n newWidth = Math.max(50, snapValue(resizeStateRef.current.startWidth + handleDeltaX));\n }\n if (resizeStateRef.current.handle.includes('left') || resizeStateRef.current.handle === 'left') {\n newWidth = Math.max(50, snapValue(resizeStateRef.current.startWidth - handleDeltaX));\n newX = snapValue(element.x + handleDeltaX);\n }\n if (resizeStateRef.current.handle.includes('bottom') || resizeStateRef.current.handle === 'bottom') {\n newHeight = Math.max(50, snapValue(resizeStateRef.current.startHeight + handleDeltaY));\n }\n if (resizeStateRef.current.handle.includes('top') || resizeStateRef.current.handle === 'top') {\n newHeight = Math.max(50, snapValue(resizeStateRef.current.startHeight - handleDeltaY));\n newY = snapValue(element.y + handleDeltaY);\n }\n \n onResizeEnd({\n id: element.id,\n x: newX,\n y: newY,\n width: newWidth,\n height: newHeight,\n });\n \n // Reset resize state\n resizeStateRef.current = { isResizing: false, handle: '', startWidth: 0, startHeight: 0, startX: 0, startY: 0 };\n }\n \n if (rotateStateRef.current.isRotating) {\n const coords = lastMousePosRef.current;\n const deltaX = coords.x - rotateStateRef.current.centerX;\n const deltaY = coords.y - rotateStateRef.current.centerY;\n const currentAngle = Math.atan2(deltaY, deltaX) * 180 / Math.PI;\n const newRotation = currentAngle - rotateStateRef.current.startAngle;\n \n onRotateEnd({\n id: element.id,\n rotation: newRotation,\n });\n \n // Reset rotate state\n rotateStateRef.current = { isRotating: false, startAngle: 0, centerX: 0, centerY: 0 };\n }\n }, [element, selected, snapValue, onDragEnd, onMultiDragEnd, onResizeEnd, onRotateEnd]);\n\n useEffect(() => {\n document.addEventListener('mousemove', handleMouseMove);\n document.addEventListener('mouseup', handleMouseUp);\n return () => {\n document.removeEventListener('mousemove', handleMouseMove);\n document.removeEventListener('mouseup', handleMouseUp);\n };\n }, [handleMouseMove, handleMouseUp]);\n\n return (\n <>\n <div\n ref={elementRef}\n className=\"element\"\n data-element-id={element.id}\n style={getThemeStyle({\n position: 'absolute',\n left: element.x,\n top: element.y,\n width: element.width,\n height: element.height,\n borderRadius: '0px',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n fontSize: '14px',\n cursor: 'move',\n transform: `rotate(${element.rotation}deg)`,\n transformOrigin: `${element.origin[0] * 100}% ${element.origin[1] * 100}%`,\n userSelect: 'none',\n zIndex: element.zIndex,\n }, isSelected ? theme.element : undefined)}\n onMouseDown={handleMouseDown}\n >\n {children}\n </div>\n \n {/* Origin indicator */}\n {isSelected && showOrigin && (\n <div\n data-origin-id={element.id}\n style={{\n ...theme.origin,\n left: element.x + element.origin[0] * element.width - 3,\n top: element.y + element.origin[1] * element.height - 3,\n transform: 'none',\n transformOrigin: 'center',\n }}\n />\n )}\n \n {/* Selection outline and handles */}\n {isSelected && (\n <div\n ref={selectionOutlineRef}\n style={getThemeStyle({\n position: 'absolute',\n left: element.x,\n top: element.y,\n width: element.width,\n height: element.height,\n outline: '1px solid #007bff',\n borderRadius: '0px',\n transform: `rotate(${element.rotation}deg)`,\n transformOrigin: `${element.origin[0] * 100}% ${element.origin[1] * 100}%`,\n pointerEvents: 'none',\n zIndex: 9999,\n }, theme.element)}\n >\n {/* Resize handles - positioned relative to selection outline */}\n <div \n className=\"resize-handle\" \n data-element-id={element.id} \n data-handle=\"top-left\" \n style={getResizeHandlerStyle('topLeft', {\n position: 'absolute', \n left: -4.5, \n top: -4.5, \n width: 8, \n height: 8, \n backgroundColor: 'white', \n outline: '1px solid #007bff', \n borderRadius: 0, \n cursor: 'nw-resize', \n pointerEvents: 'auto',\n zIndex: 10000,\n })} \n onMouseDown={(e) => handleResizeMouseDown(e, 'top-left')}\n />\n <div \n className=\"resize-handle\" \n data-element-id={element.id} \n data-handle=\"top\" \n style={getResizeHandlerStyle('top', {\n position: 'absolute', \n left: '50%', \n top: -4.5, \n width: 8, \n height: 8, \n backgroundColor: 'white', \n outline: '1px solid #007bff', \n borderRadius: 0, \n cursor: 'n-resize', \n transform: 'translateX(-50%)', \n pointerEvents: 'auto',\n zIndex: 10000,\n })} \n onMouseDown={(e) => handleResizeMouseDown(e, 'top')}\n />\n <div \n className=\"resize-handle\" \n data-element-id={element.id} \n data-handle=\"top-right\" \n style={getResizeHandlerStyle('topRight', {\n position: 'absolute', \n right: -4.5, \n top: -4.5, \n width: 8, \n height: 8, \n backgroundColor: 'white', \n outline: '1px solid #007bff', \n borderRadius: 0, \n cursor: 'ne-resize', \n pointerEvents: 'auto',\n zIndex: 10000,\n })} \n onMouseDown={(e) => handleResizeMouseDown(e, 'top-right')}\n />\n <div \n className=\"resize-handle\" \n data-element-id={element.id} \n data-handle=\"right\" \n style={getResizeHandlerStyle('right', {\n position: 'absolute', \n right: -4.5, \n top: '50%', \n width: 8, \n height: 8, \n backgroundColor: 'white', \n outline: '1px solid #007bff', \n borderRadius: 0, \n cursor: 'e-resize', \n transform: 'translateY(-50%)', \n pointerEvents: 'auto',\n zIndex: 10000,\n })} \n onMouseDown={(e) => handleResizeMouseDown(e, 'right')}\n />\n <div \n className=\"resize-handle\" \n data-element-id={element.id} \n data-handle=\"bottom-right\" \n style={getResizeHandlerStyle('bottomRight', {\n position: 'absolute', \n right: -4.5, \n bottom: -4, \n width: 8, \n height: 8, \n backgroundColor: 'white', \n outline: '1px solid #007bff', \n borderRadius: 0, \n cursor: 'se-resize', \n pointerEvents: 'auto',\n zIndex: 10000,\n })} \n onMouseDown={(e) => handleResizeMouseDown(e, 'bottom-right')}\n />\n <div \n className=\"resize-handle\" \n data-element-id={element.id} \n data-handle=\"bottom\" \n style={getResizeHandlerStyle('bottom', {\n position: 'absolute', \n left: '50%', \n bottom: -4.5, \n width: 8, \n height: 8, \n backgroundColor: 'white', \n outline: '1px solid #007bff', \n borderRadius: 0, \n cursor: 's-resize', \n transform: 'translateX(-50%)', \n pointerEvents: 'auto',\n zIndex: 10000,\n })} \n onMouseDown={(e) => handleResizeMouseDown(e, 'bottom')}\n />\n <div \n className=\"resize-handle\" \n data-element-id={element.id} \n data-handle=\"bottom-left\" \n style={getResizeHandlerStyle('bottomLeft', {\n position: 'absolute', \n left: -4.5, \n bottom: -4, \n width: 8, \n height: 8, \n backgroundColor: 'white', \n outline: '1px solid #007bff', \n borderRadius: 0, \n cursor: 'sw-resize', \n pointerEvents: 'auto',\n zIndex: 10000,\n })} \n onMouseDown={(e) => handleResizeMouseDown(e, 'bottom-left')}\n />\n <div \n className=\"resize-handle\" \n data-element-id={element.id} \n data-handle=\"left\" \n style={getResizeHandlerStyle('left', {\n position: 'absolute', \n left: -4.5, \n top: '50%', \n width: 8, \n height: 8, \n backgroundColor: 'white', \n outline: '1px solid #007bff', \n borderRadius: 0, \n cursor: 'w-resize', \n transform: 'translateY(-50%)', \n pointerEvents: 'auto',\n zIndex: 10000,\n })} \n onMouseDown={(e) => handleResizeMouseDown(e, 'left')}\n />\n \n {/* Rotation handles - positioned relative to selection outline */}\n <div \n className=\"rotate-handle\" \n data-element-id={element.id} \n data-handle=\"rotate-top-left\" \n style={getRotationHandlerSty