{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "elastic-line",
  "type": "registry:block",
  "title": "Elastic line",
  "description": "Elastic line",
  "files": [
    {
      "path": "components/usages/elasticlineusage.tsx",
      "content": "\"use client\";\r\n\r\nimport React from \"react\";\r\n\r\nimport ElasticLine from \"@/registry/open-source/elastic-line\";\r\n\r\nexport default function Usage() {\r\n\treturn (\r\n\t\t<div className=\"h-screen w-full flex items-center justify-center relative overflow-hidden bg-background\">\r\n\t\t\t<div className=\"w-full px-6 sm:px-8 md:px-12\">\r\n\t\t\t\t<ElasticLine\r\n\t\t\t\t\treleaseThreshold={50}\r\n\t\t\t\t\tstrokeWidth={1}\r\n\t\t\t\t\tanimateInTransition={{\r\n\t\t\t\t\t\ttype: \"spring\",\r\n\t\t\t\t\t\tstiffness: 300,\r\n\t\t\t\t\t\tdamping: 30,\r\n\t\t\t\t\t\tdelay: 0.15,\r\n\t\t\t\t\t}}\r\n\t\t\t\t/>\r\n\t\t\t</div>{\" \"}\r\n\t\t</div>\r\n\t);\r\n}\r\n",
      "type": "registry:block",
      "target": "~/example.tsx"
    },
    {
      "path": "components/usages/elasticlineusage.tsx",
      "content": "\"use client\";\r\n\r\nimport React from \"react\";\r\n\r\nimport ElasticLine from \"@/registry/open-source/elastic-line\";\r\n\r\nexport default function Usage() {\r\n\treturn (\r\n\t\t<div className=\"h-screen w-full flex items-center justify-center relative overflow-hidden bg-background\">\r\n\t\t\t<div className=\"w-full px-6 sm:px-8 md:px-12\">\r\n\t\t\t\t<ElasticLine\r\n\t\t\t\t\treleaseThreshold={50}\r\n\t\t\t\t\tstrokeWidth={1}\r\n\t\t\t\t\tanimateInTransition={{\r\n\t\t\t\t\t\ttype: \"spring\",\r\n\t\t\t\t\t\tstiffness: 300,\r\n\t\t\t\t\t\tdamping: 30,\r\n\t\t\t\t\t\tdelay: 0.15,\r\n\t\t\t\t\t}}\r\n\t\t\t\t/>\r\n\t\t\t</div>{\" \"}\r\n\t\t</div>\r\n\t);\r\n}\r\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/open-source/elastic-line.tsx",
      "content": "\"use client\";\n\nimport React, { useEffect, useRef, useState } from \"react\";\n\nimport { useDimensions } from \"@/registry/utilities/useDimensions\";\nimport { useElasticLineEvents } from \"@/registry/utilities/useElasticeLineEvents\";\nimport {\n\tanimate,\n\tmotion,\n\tuseAnimationFrame,\n\tuseMotionValue,\n\tValueAnimationTransition,\n} from \"motion/react\";\n\n// Credit:\n// https://www.fancycomponents.dev/docs/components/physics/elastic-line\n\ninterface ElasticLineProps {\n\tisVertical?: boolean;\n\tgrabThreshold?: number;\n\treleaseThreshold?: number;\n\tstrokeWidth?: number;\n\ttransition?: ValueAnimationTransition;\n\tanimateInTransition?: ValueAnimationTransition;\n\tclassName?: string;\n}\n\nconst ElasticLine: React.FC<ElasticLineProps> = ({\n\tisVertical = false,\n\tgrabThreshold = 5,\n\treleaseThreshold = 100,\n\tstrokeWidth = 1,\n\ttransition = {\n\t\ttype: \"spring\",\n\t\tstiffness: 400,\n\t\tdamping: 5,\n\t},\n\tanimateInTransition = {\n\t\tduration: 0.3,\n\t\tease: \"easeInOut\",\n\t},\n\tclassName,\n}) => {\n\tconst containerRef = useRef<SVGSVGElement>(null);\n\tconst dimensions = useDimensions(containerRef);\n\tconst pathRef = useRef<SVGPathElement>(null);\n\tconst [hasAnimatedIn, setHasAnimatedIn] = useState(false);\n\n\t// Clamp releaseThreshold to container dimensions\n\tconst clampedReleaseThreshold = Math.min(\n\t\treleaseThreshold,\n\t\tisVertical ? dimensions.width / 2 : dimensions.height / 2\n\t);\n\n\tconst { isGrabbed, controlPoint } = useElasticLineEvents(\n\t\tcontainerRef,\n\t\tisVertical,\n\t\tgrabThreshold,\n\t\tclampedReleaseThreshold\n\t);\n\n\tconst x = useMotionValue(dimensions.width / 2);\n\tconst y = useMotionValue(dimensions.height / 2);\n\tconst pathLength = useMotionValue(0);\n\n\tuseEffect(() => {\n\t\t// Initial draw animation\n\t\tif (!hasAnimatedIn && dimensions.width > 0 && dimensions.height > 0) {\n\t\t\tanimate(pathLength, 1, {\n\t\t\t\t...animateInTransition,\n\t\t\t\tonComplete: () => setHasAnimatedIn(true),\n\t\t\t});\n\t\t}\n\t\tx.set(dimensions.width / 2);\n\t\ty.set(dimensions.height / 2);\n\t}, [dimensions, hasAnimatedIn]);\n\n\tuseEffect(() => {\n\t\tif (!isGrabbed && hasAnimatedIn) {\n\t\t\tanimate(x, dimensions.width / 2, transition);\n\t\t\tanimate(y, dimensions.height / 2, transition);\n\t\t}\n\t}, [isGrabbed]);\n\n\tuseAnimationFrame(() => {\n\t\tif (isGrabbed) {\n\t\t\tx.set(controlPoint.x);\n\t\t\ty.set(controlPoint.y);\n\t\t}\n\n\t\tconst controlX = hasAnimatedIn ? x.get() : dimensions.width / 2;\n\t\tconst controlY = hasAnimatedIn ? y.get() : dimensions.height / 2;\n\n\t\tpathRef.current?.setAttribute(\n\t\t\t\"d\",\n\t\t\tisVertical\n\t\t\t\t? `M${dimensions.width / 2} 0Q${controlX} ${controlY} ${\n\t\t\t\t\t\tdimensions.width / 2\n\t\t\t\t\t} ${dimensions.height}`\n\t\t\t\t: `M0 ${dimensions.height / 2}Q${controlX} ${controlY} ${\n\t\t\t\t\t\tdimensions.width\n\t\t\t\t\t} ${dimensions.height / 2}`\n\t\t);\n\t});\n\n\treturn (\n\t\t<svg\n\t\t\tref={containerRef}\n\t\t\tclassName={`w-full ${className}`}\n\t\t\tviewBox={`0 0 ${dimensions.width} ${dimensions.height}`}\n\t\t\tpreserveAspectRatio=\"none\"\n\t\t>\n\t\t\t<motion.path\n\t\t\t\tref={pathRef}\n\t\t\t\tstroke=\"currentColor\"\n\t\t\t\tstrokeWidth={strokeWidth}\n\t\t\t\tinitial={{ pathLength: 0 }}\n\t\t\t\tstyle={{ pathLength }}\n\t\t\t\tfill=\"none\"\n\t\t\t/>\n\t\t</svg>\n\t);\n};\n\nexport default ElasticLine;\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/utilities/useDimensions.ts",
      "content": "import { RefObject, useEffect, useState } from \"react\";\n\ninterface Dimensions {\n\twidth: number;\n\theight: number;\n}\n\nexport function useDimensions(\n\tref: RefObject<HTMLElement | SVGElement>\n): Dimensions {\n\tconst [dimensions, setDimensions] = useState<Dimensions>({\n\t\twidth: 0,\n\t\theight: 0,\n\t});\n\n\tuseEffect(() => {\n\t\tconst updateDimensions = () => {\n\t\t\tif (ref?.current) {\n\t\t\t\tconst { width, height } = ref.current.getBoundingClientRect();\n\t\t\t\tsetDimensions({ width, height });\n\t\t\t}\n\t\t};\n\n\t\tupdateDimensions();\n\t\twindow.addEventListener(\"resize\", updateDimensions);\n\n\t\treturn () => window.removeEventListener(\"resize\", updateDimensions);\n\t}, [ref]);\n\n\treturn dimensions;\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/utilities/useElasticeLineEvents.ts",
      "content": "import { useEffect, useState } from \"react\"\nimport { useMousePosition } from \"./elasticLinePosition\";\nimport { useDimensions } from \"./useDimensions\";\n\n\n\ninterface ElasticLineEvents {\n    isGrabbed: boolean\n    controlPoint: { x: number; y: number }\n}\n\nexport function useElasticLineEvents(\n    containerRef: React.RefObject<SVGSVGElement>,\n    isVertical: boolean,\n    grabThreshold: number,\n    releaseThreshold: number\n): ElasticLineEvents {\n    const mousePosition = useMousePosition(containerRef)\n    const dimensions = useDimensions(containerRef)\n    const [isGrabbed, setIsGrabbed] = useState(false)\n    const [controlPoint, setControlPoint] = useState({\n        x: dimensions.width / 2,\n        y: dimensions.height / 2,\n    })\n\n    useEffect(() => {\n        if (containerRef.current) {\n            const { width, height } = dimensions\n            const x = mousePosition.x\n            const y = mousePosition.y\n\n            // Check if mouse is outside container bounds\n            const isOutsideBounds = x < 0 || x > width || y < 0 || y > height\n\n            if (isOutsideBounds) {\n                setIsGrabbed(false)\n                return\n            }\n\n            let distance: number\n            let newControlPoint: { x: number; y: number }\n\n            if (isVertical) {\n                const midX = width / 2\n                distance = Math.abs(x - midX)\n                newControlPoint = {\n                    x: midX + 2.2 * (x - midX),\n                    y: y,\n                }\n            } else {\n                const midY = height / 2\n                distance = Math.abs(y - midY)\n                newControlPoint = {\n                    x: x,\n                    y: midY + 2.2 * (y - midY),\n                }\n            }\n\n            setControlPoint(newControlPoint)\n\n            if (!isGrabbed && distance < grabThreshold) {\n                setIsGrabbed(true)\n            } else if (isGrabbed && distance > releaseThreshold) {\n                setIsGrabbed(false)\n            }\n        }\n    }, [mousePosition, isVertical, isGrabbed, grabThreshold, releaseThreshold])\n\n    return { isGrabbed, controlPoint }\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/utilities/elasticLinePosition.ts",
      "content": "import { RefObject, useEffect, useState } from \"react\"\n\nexport const useMousePosition = (\n    containerRef?: RefObject<HTMLElement | SVGElement>\n) => {\n    const [position, setPosition] = useState({ x: 0, y: 0 })\n\n    useEffect(() => {\n        const updatePosition = (x: number, y: number) => {\n            if (containerRef && containerRef.current) {\n                const rect = containerRef.current.getBoundingClientRect()\n                const relativeX = x - rect.left\n                const relativeY = y - rect.top\n\n                // Calculate relative position even when outside the container\n                setPosition({ x: relativeX, y: relativeY })\n            } else {\n                setPosition({ x, y })\n            }\n        }\n\n        const handleMouseMove = (ev: MouseEvent) => {\n            updatePosition(ev.clientX, ev.clientY)\n        }\n\n        const handleTouchMove = (ev: TouchEvent) => {\n            const touch = ev.touches[0]\n            updatePosition(touch.clientX, touch.clientY)\n        }\n\n        // Listen for both mouse and touch events\n        window.addEventListener(\"mousemove\", handleMouseMove)\n        window.addEventListener(\"touchmove\", handleTouchMove)\n\n        return () => {\n            window.removeEventListener(\"mousemove\", handleMouseMove)\n            window.removeEventListener(\"touchmove\", handleTouchMove)\n        }\n    }, [containerRef])\n\n    return position\n}\n",
      "type": "registry:ui"
    }
  ]
}