{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "attractor",
  "type": "registry:block",
  "title": "Attractor",
  "description": "Attractor",
  "files": [
    {
      "path": "components/usages/attractorusage.tsx",
      "content": "import Attractor, { MatterBody } from \"@/registry/open-source/attractor\";\r\nimport { useWindowSize } from \"@/registry/utilities/useWindowSize\";\r\n\r\nexport default function AttractorPreview() {\r\n\tconst screenSize = useWindowSize();\r\n\r\n\tconst getImageCount = () => {\r\n\t\tif (screenSize.width < 150) return 50;\r\n\t\tif (screenSize.width < 750) return 60;\r\n\t\tif (screenSize.width < 1500) return 70;\r\n\t\treturn 80;\r\n\t};\r\n\r\n\tconst getMaxSize = () => {\r\n\t\tif (screenSize.width < 150) return 40;\r\n\t\tif (screenSize.width < 750) return 50;\r\n\t\treturn 60;\r\n\t};\r\n\r\n\tconst getMinSize = () => {\r\n\t\tif (screenSize.width < 150) return 10;\r\n\t\tif (screenSize.width < 750) return 20;\r\n\t\treturn 20;\r\n\t};\r\n\r\n\treturn (\r\n\t\t<div className=\"w-full h-full flex flex-col relative justify-center items-center md:items-end bg-background\">\r\n\t\t\t<div>\r\n\t\t\t\t<p className=\"z-20 text-2xl sm:text-3xl md:text-3xl text-foreground dark:text-muted md:pr-24\">\r\n\t\t\t\t\tjoin the <span className=\"font-calendas  italic\">community</span>\r\n\t\t\t\t</p>\r\n\t\t\t</div>\r\n\t\t\t<Attractor\r\n\t\t\t\tattractorPoint={{ x: \"33%\", y: \"50%\" }}\r\n\t\t\t\tattractorStrength={0.0005}\r\n\t\t\t\tcursorStrength={-0.004}\r\n\t\t\t\tcursorFieldRadius={screenSize.width < 150 ? 100 : 200}\r\n\t\t\t\tclassName=\"w-full h-full\"\r\n\t\t\t>\r\n\t\t\t\t{[...Array(getImageCount())].map((_, i) => {\r\n\t\t\t\t\tconst size = Math.max(\r\n\t\t\t\t\t\tgetMinSize(),\r\n\t\t\t\t\t\tMath.random() * getMaxSize()\r\n\t\t\t\t\t);\r\n\t\t\t\t\treturn (\r\n\t\t\t\t\t\t<MatterBody\r\n\t\t\t\t\t\t\tkey={i + \"attractor-example\"}\r\n\t\t\t\t\t\t\tmatterBodyOptions={{ friction: 0.5, restitution: 0.2 }}\r\n\t\t\t\t\t\t\tx={`${Math.random() * 100}%`}\r\n\t\t\t\t\t\t\ty={`${Math.random() * 30}%`}\r\n\t\t\t\t\t\t>\r\n\t\t\t\t\t\t\t<img\r\n\t\t\t\t\t\t\t\tsrc={`https://randomuser.me/api/portraits/${\r\n\t\t\t\t\t\t\t\t\ti % 2 === 0 ? \"men\" : \"women\"\r\n\t\t\t\t\t\t\t\t}/${i}.jpg`}\r\n\t\t\t\t\t\t\t\talt={`Avatar ${i}`}\r\n\t\t\t\t\t\t\t\tclassName=\"rounded-full object-cover hover:cursor-pointer\"\r\n\t\t\t\t\t\t\t\tstyle={{\r\n\t\t\t\t\t\t\t\t\twidth: `${size}px`,\r\n\t\t\t\t\t\t\t\t\theight: `${size}px`,\r\n\t\t\t\t\t\t\t\t}}\r\n\t\t\t\t\t\t\t/>\r\n\t\t\t\t\t\t</MatterBody>\r\n\t\t\t\t\t);\r\n\t\t\t\t})}\r\n\t\t\t</Attractor>\r\n\t\t</div>\r\n\t);\r\n}\r\n",
      "type": "registry:block",
      "target": "~/example.tsx"
    },
    {
      "path": "components/usages/attractorusage.tsx",
      "content": "import Attractor, { MatterBody } from \"@/registry/open-source/attractor\";\r\nimport { useWindowSize } from \"@/registry/utilities/useWindowSize\";\r\n\r\nexport default function AttractorPreview() {\r\n\tconst screenSize = useWindowSize();\r\n\r\n\tconst getImageCount = () => {\r\n\t\tif (screenSize.width < 150) return 50;\r\n\t\tif (screenSize.width < 750) return 60;\r\n\t\tif (screenSize.width < 1500) return 70;\r\n\t\treturn 80;\r\n\t};\r\n\r\n\tconst getMaxSize = () => {\r\n\t\tif (screenSize.width < 150) return 40;\r\n\t\tif (screenSize.width < 750) return 50;\r\n\t\treturn 60;\r\n\t};\r\n\r\n\tconst getMinSize = () => {\r\n\t\tif (screenSize.width < 150) return 10;\r\n\t\tif (screenSize.width < 750) return 20;\r\n\t\treturn 20;\r\n\t};\r\n\r\n\treturn (\r\n\t\t<div className=\"w-full h-full flex flex-col relative justify-center items-center md:items-end bg-background\">\r\n\t\t\t<div>\r\n\t\t\t\t<p className=\"z-20 text-2xl sm:text-3xl md:text-3xl text-foreground dark:text-muted md:pr-24\">\r\n\t\t\t\t\tjoin the <span className=\"font-calendas  italic\">community</span>\r\n\t\t\t\t</p>\r\n\t\t\t</div>\r\n\t\t\t<Attractor\r\n\t\t\t\tattractorPoint={{ x: \"33%\", y: \"50%\" }}\r\n\t\t\t\tattractorStrength={0.0005}\r\n\t\t\t\tcursorStrength={-0.004}\r\n\t\t\t\tcursorFieldRadius={screenSize.width < 150 ? 100 : 200}\r\n\t\t\t\tclassName=\"w-full h-full\"\r\n\t\t\t>\r\n\t\t\t\t{[...Array(getImageCount())].map((_, i) => {\r\n\t\t\t\t\tconst size = Math.max(\r\n\t\t\t\t\t\tgetMinSize(),\r\n\t\t\t\t\t\tMath.random() * getMaxSize()\r\n\t\t\t\t\t);\r\n\t\t\t\t\treturn (\r\n\t\t\t\t\t\t<MatterBody\r\n\t\t\t\t\t\t\tkey={i + \"attractor-example\"}\r\n\t\t\t\t\t\t\tmatterBodyOptions={{ friction: 0.5, restitution: 0.2 }}\r\n\t\t\t\t\t\t\tx={`${Math.random() * 100}%`}\r\n\t\t\t\t\t\t\ty={`${Math.random() * 30}%`}\r\n\t\t\t\t\t\t>\r\n\t\t\t\t\t\t\t<img\r\n\t\t\t\t\t\t\t\tsrc={`https://randomuser.me/api/portraits/${\r\n\t\t\t\t\t\t\t\t\ti % 2 === 0 ? \"men\" : \"women\"\r\n\t\t\t\t\t\t\t\t}/${i}.jpg`}\r\n\t\t\t\t\t\t\t\talt={`Avatar ${i}`}\r\n\t\t\t\t\t\t\t\tclassName=\"rounded-full object-cover hover:cursor-pointer\"\r\n\t\t\t\t\t\t\t\tstyle={{\r\n\t\t\t\t\t\t\t\t\twidth: `${size}px`,\r\n\t\t\t\t\t\t\t\t\theight: `${size}px`,\r\n\t\t\t\t\t\t\t\t}}\r\n\t\t\t\t\t\t\t/>\r\n\t\t\t\t\t\t</MatterBody>\r\n\t\t\t\t\t);\r\n\t\t\t\t})}\r\n\t\t\t</Attractor>\r\n\t\t</div>\r\n\t);\r\n}\r\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/open-source/attractor.tsx",
      "content": "\"use client\";\n\nimport {\n\tcreateContext,\n\tforwardRef,\n\tReactNode,\n\tuseCallback,\n\tuseContext,\n\tuseEffect,\n\tuseImperativeHandle,\n\tuseRef,\n\tuseState,\n} from \"react\";\n\nimport { calculatePosition } from \"@/registry/utilities/calculatePosition\";\nimport { cn } from \"@/registry/utilities/cn\";\nimport { parsePathToVertices } from \"@/registry/utilities/parsePathToVertices\";\nimport { useMousePositionRef } from \"@/registry/utilities/useMousePosition\";\nimport { debounce } from \"lodash\";\nimport Matter, {\n\tBodies,\n\tBody,\n\tCommon,\n\tEngine,\n\tEvents,\n\tRender,\n\tRunner,\n\tWorld,\n} from \"matter-js\";\n\n// Credit:\n// https://www.fancycomponents.dev/docs/components/physics/gravity\n\ntype GravityProps = {\n\tchildren: ReactNode;\n\tdebug?: boolean;\n\tattractorPoint?: { x: number | string; y: number | string };\n\tattractorStrength?: number;\n\tcursorStrength?: number;\n\tcursorFieldRadius?: number;\n\tresetOnResize?: boolean;\n\taddTopWall?: boolean;\n\tautoStart?: boolean;\n\tclassName?: string;\n};\n\ntype PhysicsBody = {\n\telement: HTMLElement;\n\tbody: Matter.Body;\n\tprops: MatterBodyProps;\n};\n\ntype MatterBodyProps = {\n\tchildren: ReactNode;\n\tmatterBodyOptions?: Matter.IBodyDefinition;\n\tisDraggable?: boolean;\n\tbodyType?: \"rectangle\" | \"circle\" | \"svg\";\n\tsampleLength?: number;\n\tx?: number | string;\n\ty?: number | string;\n\tangle?: number;\n\tclassName?: string;\n};\n\nexport type GravityRef = {\n\tstart: () => void;\n\tstop: () => void;\n\treset: () => void;\n};\n\nconst GravityContext = createContext<{\n\tregisterElement: (\n\t\tid: string,\n\t\telement: HTMLElement,\n\t\tprops: MatterBodyProps\n\t) => void;\n\tunregisterElement: (id: string) => void;\n} | null>(null);\n\nexport const MatterBody = ({\n\tchildren,\n\tclassName,\n\tmatterBodyOptions = {\n\t\tfriction: 0.1,\n\t\trestitution: 0.1,\n\t\tdensity: 0.001,\n\t\tisStatic: false,\n\t},\n\tbodyType = \"rectangle\",\n\tisDraggable = true,\n\tsampleLength = 15,\n\tx = 0,\n\ty = 0,\n\tangle = 0,\n\t...props\n}: MatterBodyProps) => {\n\tconst elementRef = useRef<HTMLDivElement>(null);\n\tconst idRef = useRef(Math.random().toString(36).substring(7));\n\tconst context = useContext(GravityContext);\n\n\tuseEffect(() => {\n\t\tif (!elementRef.current || !context) return;\n\t\tcontext.registerElement(idRef.current, elementRef.current, {\n\t\t\tchildren,\n\t\t\tmatterBodyOptions,\n\t\t\tbodyType,\n\t\t\tsampleLength,\n\t\t\tisDraggable,\n\t\t\tx,\n\t\t\ty,\n\t\t\tangle,\n\t\t\t...props,\n\t\t});\n\n\t\treturn () => context.unregisterElement(idRef.current);\n\t}, [props, children, matterBodyOptions, isDraggable]);\n\n\treturn (\n\t\t<div ref={elementRef} className={cn(\"absolute\", className)}>\n\t\t\t{children}\n\t\t</div>\n\t);\n};\n\nconst Attractor = forwardRef<GravityRef, GravityProps>(\n\t(\n\t\t{\n\t\t\tchildren,\n\t\t\tdebug = false,\n\t\t\tattractorPoint = { x: 0.5, y: 0.5 },\n\t\t\tattractorStrength = 0.001,\n\t\t\tcursorStrength = 0.0005,\n\t\t\tcursorFieldRadius = 100,\n\t\t\tresetOnResize = true,\n\t\t\taddTopWall = true,\n\t\t\tautoStart = true,\n\t\t\tclassName,\n\t\t\t...props\n\t\t},\n\t\tref\n\t) => {\n\t\tconst canvas = useRef<HTMLDivElement>(null);\n\t\tconst engine = useRef(Engine.create());\n\t\tconst render = useRef<Render>();\n\t\tconst runner = useRef<Runner>();\n\t\tconst bodiesMap = useRef(new Map<string, PhysicsBody>());\n\t\tconst frameId = useRef<number>();\n\t\tconst [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 });\n\t\tconst mouseRef = useMousePositionRef(canvas);\n\n\t\tconst isRunning = useRef(false);\n\n\t\t// Register Matter.js body in the physics world\n\t\tconst registerElement = useCallback(\n\t\t\t(id: string, element: HTMLElement, props: MatterBodyProps) => {\n\t\t\t\tif (!canvas.current) return;\n\t\t\t\tconst width = element.offsetWidth;\n\t\t\t\tconst height = element.offsetHeight;\n\t\t\t\tconst canvasRect = canvas.current!.getBoundingClientRect();\n\n\t\t\t\tconst angle = (props.angle || 0) * (Math.PI / 180);\n\n\t\t\t\tconst x = calculatePosition(props.x, canvasRect.width, width);\n\t\t\t\tconst y = calculatePosition(props.y, canvasRect.height, height);\n\n\t\t\t\tlet body;\n\t\t\t\tif (props.bodyType === \"circle\") {\n\t\t\t\t\tconst radius = Math.max(width, height) / 2;\n\t\t\t\t\tbody = Bodies.circle(x, y, radius, {\n\t\t\t\t\t\t...props.matterBodyOptions,\n\t\t\t\t\t\tangle: angle,\n\t\t\t\t\t\trender: {\n\t\t\t\t\t\t\tfillStyle: debug ? \"#888888\" : \"#00000000\",\n\t\t\t\t\t\t\tstrokeStyle: debug ? \"#333333\" : \"#00000000\",\n\t\t\t\t\t\t\tlineWidth: debug ? 3 : 0,\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t} else if (props.bodyType === \"svg\") {\n\t\t\t\t\tconst paths = element.querySelectorAll(\"path\");\n\t\t\t\t\tconst vertexSets: Matter.Vector[][] = [];\n\n\t\t\t\t\tpaths.forEach((path) => {\n\t\t\t\t\t\tconst d = path.getAttribute(\"d\");\n\t\t\t\t\t\tconst p = parsePathToVertices(d!, props.sampleLength);\n\t\t\t\t\t\tvertexSets.push(p);\n\t\t\t\t\t});\n\n\t\t\t\t\tbody = Bodies.fromVertices(x, y, vertexSets, {\n\t\t\t\t\t\t...props.matterBodyOptions,\n\t\t\t\t\t\tangle: angle,\n\t\t\t\t\t\trender: {\n\t\t\t\t\t\t\tfillStyle: debug ? \"#888888\" : \"#00000000\",\n\t\t\t\t\t\t\tstrokeStyle: debug ? \"#333333\" : \"#00000000\",\n\t\t\t\t\t\t\tlineWidth: debug ? 3 : 0,\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\tbody = Bodies.rectangle(x, y, width, height, {\n\t\t\t\t\t\t...props.matterBodyOptions,\n\t\t\t\t\t\tangle: angle,\n\t\t\t\t\t\trender: {\n\t\t\t\t\t\t\tfillStyle: debug ? \"#888888\" : \"#00000000\",\n\t\t\t\t\t\t\tstrokeStyle: debug ? \"#333333\" : \"#00000000\",\n\t\t\t\t\t\t\tlineWidth: debug ? 3 : 0,\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tif (body) {\n\t\t\t\t\tWorld.add(engine.current.world, [body]);\n\t\t\t\t\tbodiesMap.current.set(id, { element, body, props });\n\t\t\t\t}\n\t\t\t},\n\t\t\t[debug]\n\t\t);\n\n\t\t// Unregister Matter.js body from the physics world\n\t\tconst unregisterElement = useCallback((id: string) => {\n\t\t\tconst body = bodiesMap.current.get(id);\n\t\t\tif (body) {\n\t\t\t\tWorld.remove(engine.current.world, body.body);\n\t\t\t\tbodiesMap.current.delete(id);\n\t\t\t}\n\t\t}, []);\n\n\t\t// Keep react elements in sync with the physics world\n\t\tconst updateElements = useCallback(() => {\n\t\t\tbodiesMap.current.forEach(({ element, body }) => {\n\t\t\t\tconst { x, y } = body.position;\n\t\t\t\tconst rotation = body.angle * (180 / Math.PI);\n\n\t\t\t\telement.style.transform = `translate(${\n\t\t\t\t\tx - element.offsetWidth / 2\n\t\t\t\t}px, ${y - element.offsetHeight / 2}px) rotate(${rotation}deg)`;\n\t\t\t});\n\n\t\t\tframeId.current = requestAnimationFrame(updateElements);\n\t\t}, []);\n\n\t\tconst initializeRenderer = useCallback(() => {\n\t\t\tif (!canvas.current) return;\n\n\t\t\tconst height = canvas.current.offsetHeight;\n\t\t\tconst width = canvas.current.offsetWidth;\n\n\t\t\tCommon.setDecomp(require(\"poly-decomp\"));\n\n\t\t\t// Remove default gravity\n\t\t\tengine.current.gravity.x = 0;\n\t\t\tengine.current.gravity.y = 0;\n\n\t\t\trender.current = Render.create({\n\t\t\t\telement: canvas.current,\n\t\t\t\tengine: engine.current,\n\t\t\t\toptions: {\n\t\t\t\t\twidth,\n\t\t\t\t\theight,\n\t\t\t\t\twireframes: false,\n\t\t\t\t\tbackground: \"#00000000\",\n\t\t\t\t},\n\t\t\t});\n\n\t\t\t// Add walls\n\t\t\tconst walls = [\n\t\t\t\t// Floor\n\t\t\t\tBodies.rectangle(width / 2, height + 10, width, 20, {\n\t\t\t\t\tisStatic: true,\n\t\t\t\t\tfriction: 1,\n\t\t\t\t\trender: {\n\t\t\t\t\t\tvisible: debug,\n\t\t\t\t\t},\n\t\t\t\t}),\n\n\t\t\t\t// Right wall\n\t\t\t\tBodies.rectangle(width + 10, height / 2, 20, height, {\n\t\t\t\t\tisStatic: true,\n\t\t\t\t\tfriction: 1,\n\t\t\t\t\trender: {\n\t\t\t\t\t\tvisible: debug,\n\t\t\t\t\t},\n\t\t\t\t}),\n\n\t\t\t\t// Left wall\n\t\t\t\tBodies.rectangle(-10, height / 2, 20, height, {\n\t\t\t\t\tisStatic: true,\n\t\t\t\t\tfriction: 1,\n\t\t\t\t\trender: {\n\t\t\t\t\t\tvisible: debug,\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t];\n\n\t\t\tconst topWall = addTopWall\n\t\t\t\t? Bodies.rectangle(width / 2, -10, width, 20, {\n\t\t\t\t\t\tisStatic: true,\n\t\t\t\t\t\tfriction: 1,\n\t\t\t\t\t\trender: {\n\t\t\t\t\t\t\tvisible: debug,\n\t\t\t\t\t\t},\n\t\t\t\t\t})\n\t\t\t\t: null;\n\n\t\t\tif (topWall) {\n\t\t\t\twalls.push(topWall);\n\t\t\t}\n\n\t\t\tWorld.add(engine.current.world, [...walls]);\n\n\t\t\trunner.current = Runner.create();\n\t\t\tRender.run(render.current);\n\t\t\tupdateElements();\n\t\t\trunner.current.enabled = false;\n\n\t\t\tif (autoStart) {\n\t\t\t\trunner.current.enabled = true;\n\t\t\t\tstartEngine();\n\t\t\t}\n\n\t\t\t// Add force application before update\n\t\t\tEvents.on(engine.current, \"beforeUpdate\", () => {\n\t\t\t\tconst bodies = engine.current.world.bodies.filter(\n\t\t\t\t\t(body) => !body.isStatic\n\t\t\t\t);\n\n\t\t\t\t// Calculate attractor position in pixels\n\t\t\t\tconst attractorX =\n\t\t\t\t\ttypeof attractorPoint.x === \"string\"\n\t\t\t\t\t\t? (width * parseFloat(attractorPoint.x)) / 100\n\t\t\t\t\t\t: width * attractorPoint.x;\n\t\t\t\tconst attractorY =\n\t\t\t\t\ttypeof attractorPoint.y === \"string\"\n\t\t\t\t\t\t? (height * parseFloat(attractorPoint.y)) / 100\n\t\t\t\t\t\t: height * attractorPoint.y;\n\n\t\t\t\tbodies.forEach((body) => {\n\t\t\t\t\t// Apply attractor force\n\t\t\t\t\tconst dx = attractorX - body.position.x;\n\t\t\t\t\tconst dy = attractorY - body.position.y;\n\t\t\t\t\tconst distance = Math.sqrt(dx * dx + dy * dy);\n\n\t\t\t\t\tif (distance > 0) {\n\t\t\t\t\t\tconst force = {\n\t\t\t\t\t\t\tx: (dx / distance) * attractorStrength * body.mass,\n\t\t\t\t\t\t\ty: (dy / distance) * attractorStrength * body.mass,\n\t\t\t\t\t\t};\n\t\t\t\t\t\tBody.applyForce(body, body.position, force);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Apply cursor force if mouse is present\n\t\t\t\t\tif (\n\t\t\t\t\t\tmouseRef.current?.x &&\n\t\t\t\t\t\tmouseRef.current?.y &&\n\t\t\t\t\t\tmouseRef.current.x > 0 &&\n\t\t\t\t\t\tmouseRef.current.y > 0\n\t\t\t\t\t) {\n\t\t\t\t\t\tconst mdx = mouseRef.current.x - body.position.x;\n\t\t\t\t\t\tconst mdy = mouseRef.current.y - body.position.y;\n\t\t\t\t\t\tconst mouseDistance = Math.sqrt(mdx * mdx + mdy * mdy);\n\n\t\t\t\t\t\tif (mouseDistance > 0 && mouseDistance < cursorFieldRadius) {\n\t\t\t\t\t\t\tconst mouseForce = {\n\t\t\t\t\t\t\t\tx: (mdx / mouseDistance) * cursorStrength * body.mass,\n\t\t\t\t\t\t\t\ty: (mdy / mouseDistance) * cursorStrength * body.mass,\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\tBody.applyForce(body, body.position, mouseForce);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t});\n\t\t}, [\n\t\t\tupdateElements,\n\t\t\tdebug,\n\t\t\tautoStart,\n\t\t\tattractorPoint,\n\t\t\tattractorStrength,\n\t\t\tcursorStrength,\n\t\t]);\n\n\t\t// Clear the Matter.js world\n\t\tconst clearRenderer = useCallback(() => {\n\t\t\tif (frameId.current) {\n\t\t\t\tcancelAnimationFrame(frameId.current);\n\t\t\t}\n\n\t\t\tif (render.current) {\n\t\t\t\tRender.stop(render.current);\n\t\t\t\trender.current.canvas.remove();\n\t\t\t}\n\n\t\t\tif (runner.current) {\n\t\t\t\tRunner.stop(runner.current);\n\t\t\t}\n\n\t\t\tif (engine.current) {\n\t\t\t\tWorld.clear(engine.current.world, false);\n\t\t\t\tEngine.clear(engine.current);\n\t\t\t}\n\n\t\t\tbodiesMap.current.clear();\n\t\t}, []);\n\n\t\tconst handleResize = useCallback(() => {\n\t\t\tif (!canvas.current || !resetOnResize) return;\n\n\t\t\tconst newWidth = canvas.current.offsetWidth;\n\t\t\tconst newHeight = canvas.current.offsetHeight;\n\n\t\t\tsetCanvasSize({ width: newWidth, height: newHeight });\n\n\t\t\t// Clear and reinitialize\n\t\t\tclearRenderer();\n\t\t\tinitializeRenderer();\n\t\t}, [clearRenderer, initializeRenderer, resetOnResize]);\n\n\t\tconst startEngine = useCallback(() => {\n\t\t\tif (runner.current) {\n\t\t\t\trunner.current.enabled = true;\n\n\t\t\t\tRunner.run(runner.current, engine.current);\n\t\t\t}\n\t\t\tif (render.current) {\n\t\t\t\tRender.run(render.current);\n\t\t\t}\n\t\t\tframeId.current = requestAnimationFrame(updateElements);\n\t\t\tisRunning.current = true;\n\t\t}, [updateElements, canvasSize]);\n\n\t\tconst stopEngine = useCallback(() => {\n\t\t\tif (!isRunning.current) return;\n\n\t\t\tif (runner.current) {\n\t\t\t\tRunner.stop(runner.current);\n\t\t\t}\n\t\t\tif (render.current) {\n\t\t\t\tRender.stop(render.current);\n\t\t\t}\n\t\t\tif (frameId.current) {\n\t\t\t\tcancelAnimationFrame(frameId.current);\n\t\t\t}\n\t\t\tisRunning.current = false;\n\t\t}, []);\n\n\t\tconst reset = useCallback(() => {\n\t\t\tstopEngine();\n\t\t\tbodiesMap.current.forEach(({ element, body, props }) => {\n\t\t\t\tbody.angle = props.angle || 0;\n\n\t\t\t\tconst x = calculatePosition(\n\t\t\t\t\tprops.x,\n\t\t\t\t\tcanvasSize.width,\n\t\t\t\t\telement.offsetWidth\n\t\t\t\t);\n\t\t\t\tconst y = calculatePosition(\n\t\t\t\t\tprops.y,\n\t\t\t\t\tcanvasSize.height,\n\t\t\t\t\telement.offsetHeight\n\t\t\t\t);\n\t\t\t\tbody.position.x = x;\n\t\t\t\tbody.position.y = y;\n\t\t\t});\n\t\t\tupdateElements();\n\t\t\thandleResize();\n\t\t}, []);\n\n\t\tuseImperativeHandle(\n\t\t\tref,\n\t\t\t() => ({\n\t\t\t\tstart: startEngine,\n\t\t\t\tstop: stopEngine,\n\t\t\t\treset,\n\t\t\t}),\n\t\t\t[startEngine, stopEngine]\n\t\t);\n\n\t\tuseEffect(() => {\n\t\t\tif (!resetOnResize) return;\n\n\t\t\tconst debouncedResize = debounce(handleResize, 500);\n\t\t\twindow.addEventListener(\"resize\", debouncedResize);\n\n\t\t\treturn () => {\n\t\t\t\twindow.removeEventListener(\"resize\", debouncedResize);\n\t\t\t\tdebouncedResize.cancel();\n\t\t\t};\n\t\t}, [handleResize, resetOnResize]);\n\n\t\tuseEffect(() => {\n\t\t\tinitializeRenderer();\n\t\t\treturn clearRenderer;\n\t\t}, [initializeRenderer, clearRenderer]);\n\n\t\treturn (\n\t\t\t<GravityContext.Provider\n\t\t\t\tvalue={{ registerElement, unregisterElement }}\n\t\t\t>\n\t\t\t\t<div\n\t\t\t\t\tref={canvas}\n\t\t\t\t\tclassName={cn(className, \"absolute top-0 left-0 w-full h-full\")}\n\t\t\t\t\t{...props}\n\t\t\t\t>\n\t\t\t\t\t{children}\n\t\t\t\t</div>\n\t\t\t</GravityContext.Provider>\n\t\t);\n\t}\n);\n\nexport default Attractor;\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/utilities/calculatePosition.ts",
      "content": "export function calculatePosition(\n    value: number | string | undefined,\n    containerSize: number,\n    elementSize: number\n): number {\n    // Handle percentage strings (e.g. \"50%\")\n    if (typeof value === \"string\" && value.endsWith(\"%\")) {\n        const percentage = parseFloat(value) / 100\n        return containerSize * percentage\n    }\n\n    // Handle direct pixel values\n    if (typeof value === \"number\") {\n        return value\n    }\n\n    // If no value provided, center the element\n    return (containerSize - elementSize) / 2\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/utilities/cn.ts",
      "content": "import { ClassValue, clsx } from \"clsx\";\r\nimport { twMerge } from \"tailwind-merge\";\r\n\r\nexport function cn(...inputs: ClassValue[]) {\r\n\treturn twMerge(clsx(inputs));\r\n}\r\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/utilities/parsePathToVertices.ts",
      "content": "import SVGPathCommander from \"svg-path-commander\"\r\n\r\n// Function to convert SVG path `d` to vertices\r\nexport function parsePathToVertices(path: string, sampleLength = 15) {\r\n    // Convert path to absolute commands\r\n    const commander = new SVGPathCommander(path)\r\n\r\n    const points: { x: number; y: number }[] = []\r\n    let lastPoint: { x: number; y: number } | null = null\r\n\r\n    // Get total length of the path\r\n    const totalLength = commander.getTotalLength()\r\n    let length = 0\r\n\r\n    // Sample points along the path\r\n    while (length < totalLength) {\r\n        const point = commander.getPointAtLength(length)\r\n\r\n        // Only add point if it's different from the last one\r\n        if (!lastPoint || point.x !== lastPoint.x || point.y !== lastPoint.y) {\r\n            points.push({ x: point.x, y: point.y })\r\n            lastPoint = point\r\n        }\r\n\r\n        length += sampleLength\r\n    }\r\n\r\n    // Ensure we get the last point\r\n    const finalPoint = commander.getPointAtLength(totalLength)\r\n    if (\r\n        lastPoint &&\r\n        (finalPoint.x !== lastPoint.x || finalPoint.y !== lastPoint.y)\r\n    ) {\r\n        points.push({ x: finalPoint.x, y: finalPoint.y })\r\n    }\r\n\r\n    return points\r\n}",
      "type": "registry:ui"
    },
    {
      "path": "registry/utilities/useMousePosition.ts",
      "content": "import { RefObject, useEffect, useRef } from \"react\";\r\n\r\nexport const useMousePositionRef = (\r\n\tcontainerRef?: RefObject<HTMLElement | SVGElement>\r\n) => {\r\n\tconst positionRef = useRef({ x: 0, y: 0 });\r\n\r\n\tuseEffect(() => {\r\n\t\tconst updatePosition = (x: number, y: number) => {\r\n\t\t\tif (containerRef && containerRef.current) {\r\n\t\t\t\tconst rect = containerRef.current.getBoundingClientRect();\r\n\t\t\t\tconst relativeX = x - rect.left;\r\n\t\t\t\tconst relativeY = y - rect.top;\r\n\r\n\t\t\t\t// Calculate relative position even when outside the container\r\n\t\t\t\tpositionRef.current = { x: relativeX, y: relativeY };\r\n\t\t\t} else {\r\n\t\t\t\tpositionRef.current = { x, y };\r\n\t\t\t}\r\n\t\t};\r\n\r\n\t\tconst handleMouseMove = (ev: MouseEvent) => {\r\n\t\t\tupdatePosition(ev.clientX, ev.clientY);\r\n\t\t};\r\n\r\n\t\tconst handleTouchMove = (ev: TouchEvent) => {\r\n\t\t\tconst touch = ev.touches[0];\r\n\t\t\tupdatePosition(touch.clientX, touch.clientY);\r\n\t\t};\r\n\r\n\t\t// Listen for both mouse and touch events\r\n\t\twindow.addEventListener(\"mousemove\", handleMouseMove);\r\n\t\twindow.addEventListener(\"touchmove\", handleTouchMove);\r\n\r\n\t\treturn () => {\r\n\t\t\twindow.removeEventListener(\"mousemove\", handleMouseMove);\r\n\t\t\twindow.removeEventListener(\"touchmove\", handleTouchMove);\r\n\t\t};\r\n\t}, [containerRef]);\r\n\r\n\r\n\treturn positionRef;\r\n};\r\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/utilities/useWindowSize.tsx",
      "content": "import { useEffect, useState } from \"react\";\r\n\r\ninterface Size {\r\n\twidth: number | undefined;\r\n\theight: number | undefined;\r\n}\r\n\r\nexport const useWindowSize = () => {\r\n\tconst [windowSize, setWindowSize] = useState<Size>({\r\n\t\twidth: undefined,\r\n\t\theight: undefined,\r\n\t});\r\n\r\n\tuseEffect(() => {\r\n\t\tfunction handleResize() {\r\n\t\t\tsetWindowSize({\r\n\t\t\t\twidth: window.innerWidth,\r\n\t\t\t\theight: window.innerHeight,\r\n\t\t\t});\r\n\t\t}\r\n\r\n\t\twindow.addEventListener(\"resize\", handleResize);\r\n\r\n\t\thandleResize();\r\n\t\treturn () => window.removeEventListener(\"resize\", handleResize);\r\n\t}, []);\r\n\r\n\treturn windowSize;\r\n};\r\n",
      "type": "registry:ui"
    }
  ]
}