{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "OrbitImages-JS-TW",
	"title": "OrbitImages",
	"description": "SVG Path customizable orbiting images effect",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "OrbitImages/OrbitImages.jsx",
			"content": "// Component created by Dominik Koch\n// https://x.com/dominikkoch\n\nimport { useMemo, useEffect, useLayoutEffect, useRef, useState } from 'react';\nimport { motion, useMotionValue, useTransform, animate } from 'motion/react';\n\nfunction generateEllipsePath(cx, cy, rx, ry) {\n  return `M ${cx - rx} ${cy} A ${rx} ${ry} 0 1 0 ${cx + rx} ${cy} A ${rx} ${ry} 0 1 0 ${cx - rx} ${cy}`;\n}\n\nfunction generateCirclePath(cx, cy, r) {\n  return generateEllipsePath(cx, cy, r, r);\n}\n\nfunction generateSquarePath(cx, cy, size) {\n  const h = size / 2;\n  return `M ${cx - h} ${cy - h} L ${cx + h} ${cy - h} L ${cx + h} ${cy + h} L ${cx - h} ${cy + h} Z`;\n}\n\nfunction generateRectanglePath(cx, cy, w, h) {\n  const hw = w / 2;\n  const hh = h / 2;\n  return `M ${cx - hw} ${cy - hh} L ${cx + hw} ${cy - hh} L ${cx + hw} ${cy + hh} L ${cx - hw} ${cy + hh} Z`;\n}\n\nfunction generateTrianglePath(cx, cy, size) {\n  const height = (size * Math.sqrt(3)) / 2;\n  const hs = size / 2;\n  return `M ${cx} ${cy - height / 1.5} L ${cx + hs} ${cy + height / 3} L ${cx - hs} ${cy + height / 3} Z`;\n}\n\nfunction generateStarPath(cx, cy, outerR, innerR, points) {\n  const step = Math.PI / points;\n  let path = '';\n  for (let i = 0; i < 2 * points; i++) {\n    const r = i % 2 === 0 ? outerR : innerR;\n    const angle = i * step - Math.PI / 2;\n    const x = cx + r * Math.cos(angle);\n    const y = cy + r * Math.sin(angle);\n    path += i === 0 ? `M ${x} ${y}` : ` L ${x} ${y}`;\n  }\n  return path + ' Z';\n}\n\nfunction generateHeartPath(cx, cy, size) {\n  const s = size / 30;\n  return `M ${cx} ${cy + 12 * s} C ${cx - 20 * s} ${cy - 5 * s}, ${cx - 12 * s} ${cy - 18 * s}, ${cx} ${cy - 8 * s} C ${cx + 12 * s} ${cy - 18 * s}, ${cx + 20 * s} ${cy - 5 * s}, ${cx} ${cy + 12 * s}`;\n}\n\nfunction generateInfinityPath(cx, cy, w, h) {\n  const hw = w / 2;\n  const hh = h / 2;\n  return `M ${cx} ${cy} C ${cx + hw * 0.5} ${cy - hh}, ${cx + hw} ${cy - hh}, ${cx + hw} ${cy} C ${cx + hw} ${cy + hh}, ${cx + hw * 0.5} ${cy + hh}, ${cx} ${cy} C ${cx - hw * 0.5} ${cy + hh}, ${cx - hw} ${cy + hh}, ${cx - hw} ${cy} C ${cx - hw} ${cy - hh}, ${cx - hw * 0.5} ${cy - hh}, ${cx} ${cy}`;\n}\n\nfunction generateWavePath(cx, cy, w, amplitude, waves) {\n  const pts = [];\n  const segs = waves * 20;\n  const hw = w / 2;\n  for (let i = 0; i <= segs; i++) {\n    const x = cx - hw + (w * i) / segs;\n    const y = cy + Math.sin((i / segs) * waves * 2 * Math.PI) * amplitude;\n    pts.push(i === 0 ? `M ${x} ${y}` : `L ${x} ${y}`);\n  }\n  for (let i = segs; i >= 0; i--) {\n    const x = cx - hw + (w * i) / segs;\n    const y = cy - Math.sin((i / segs) * waves * 2 * Math.PI) * amplitude;\n    pts.push(`L ${x} ${y}`);\n  }\n  return pts.join(' ') + ' Z';\n}\n\nfunction OrbitItem({ item, index, totalItems, path, itemSize, rotation, progress, fill }) {\n  const itemOffset = fill ? (index / totalItems) * 100 : 0;\n\n  const offsetDistance = useTransform(progress, (p) => {\n    const offset = (((p + itemOffset) % 100) + 100) % 100;\n    return `${offset}%`;\n  });\n\n  return (\n    <motion.div\n      className=\"absolute will-change-transform select-none\"\n      style={{\n        width: itemSize,\n        height: itemSize,\n        offsetPath: `path(\"${path}\")`,\n        offsetRotate: '0deg',\n        offsetAnchor: 'center center',\n        offsetDistance,\n      }}\n    >\n      <div style={{ transform: `rotate(${-rotation}deg)` }}>{item}</div>\n    </motion.div>\n  );\n}\n\nexport default function OrbitImages({\n  images = [],\n  altPrefix = 'Orbiting image',\n  shape = 'ellipse',\n  customPath,\n  baseWidth = 1400,\n  radiusX = 700,\n  radiusY = 170,\n  radius = 300,\n  starPoints = 5,\n  starInnerRatio = 0.5,\n  rotation = -8,\n  duration = 40,\n  itemSize = 64,\n  direction = 'normal',\n  fill = true,\n  width = 100,\n  height = 100,\n  className = '',\n  showPath = false,\n  pathColor = 'rgba(0,0,0,0.1)',\n  pathWidth = 2,\n  easing = 'linear',\n  paused = false,\n  centerContent,\n  responsive = false,\n}) {\n  const containerRef = useRef(null);\n  const [scale, setScale] = useState(null);\n\n  const designCenterX = baseWidth / 2;\n  const designCenterY = baseWidth / 2;\n\n  const path = useMemo(() => {\n    switch (shape) {\n      case 'circle':\n        return generateCirclePath(designCenterX, designCenterY, radius);\n      case 'ellipse':\n        return generateEllipsePath(designCenterX, designCenterY, radiusX, radiusY);\n      case 'square':\n        return generateSquarePath(designCenterX, designCenterY, radius * 2);\n      case 'rectangle':\n        return generateRectanglePath(designCenterX, designCenterY, radiusX * 2, radiusY * 2);\n      case 'triangle':\n        return generateTrianglePath(designCenterX, designCenterY, radius * 2);\n      case 'star':\n        return generateStarPath(designCenterX, designCenterY, radius, radius * starInnerRatio, starPoints);\n      case 'heart':\n        return generateHeartPath(designCenterX, designCenterY, radius * 2);\n      case 'infinity':\n        return generateInfinityPath(designCenterX, designCenterY, radiusX * 2, radiusY * 2);\n      case 'wave':\n        return generateWavePath(designCenterX, designCenterY, radiusX * 2, radiusY, 3);\n      case 'custom':\n        return customPath || generateCirclePath(designCenterX, designCenterY, radius);\n      default:\n        return generateEllipsePath(designCenterX, designCenterY, radiusX, radiusY);\n    }\n  }, [shape, customPath, designCenterX, designCenterY, radiusX, radiusY, radius, starPoints, starInnerRatio]);\n\n  useLayoutEffect(() => {\n    if (!responsive || !containerRef.current) return;\n    const updateScale = () => {\n      if (!containerRef.current) return;\n      setScale(containerRef.current.clientWidth / baseWidth);\n    };\n    updateScale();\n    const observer = new ResizeObserver(updateScale);\n    observer.observe(containerRef.current);\n    return () => observer.disconnect();\n  }, [responsive, baseWidth]);\n\n  const progress = useMotionValue(0);\n\n  useEffect(() => {\n    if (paused) return;\n    const controls = animate(progress, direction === 'reverse' ? -100 : 100, {\n      duration,\n      ease: easing,\n      repeat: Infinity,\n      repeatType: 'loop',\n    });\n    return () => controls.stop();\n  }, [progress, duration, easing, direction, paused]);\n\n  const containerWidth = responsive ? '100%' : (typeof width === 'number' ? width : '100%');\n  const containerHeight = responsive ? 'auto' : (typeof height === 'number' ? height : (typeof width === 'number' ? width : 'auto'));\n\n  const items = images.map((src, index) => (\n    <img\n      key={src}\n      src={src}\n      alt={`${altPrefix} ${index + 1}`}\n      draggable={false}\n      className=\"w-full h-full object-contain\"\n    />\n  ));\n\n  return (\n    <div\n      ref={containerRef}\n      className={`relative mx-auto ${className}`}\n      style={{\n        width: containerWidth,\n        height: containerHeight,\n        aspectRatio: responsive ? '1 / 1' : undefined,\n      }}\n      aria-hidden=\"true\"\n    >\n      <div\n        className={responsive ? 'absolute left-1/2 top-1/2' : 'relative w-full h-full'}\n        style={{\n          width: responsive ? baseWidth : '100%',\n          height: responsive ? baseWidth : '100%',\n          transform: responsive && scale !== null ? `translate(-50%, -50%) scale(${scale})` : undefined,\n          visibility: responsive && scale === null ? 'hidden' : undefined,\n          transformOrigin: 'center center',\n        }}\n      >\n        <div\n          className=\"relative w-full h-full\"\n          style={{\n            transform: `rotate(${rotation}deg)`,\n            transformOrigin: 'center center',\n          }}\n        >\n          {showPath && (\n            <svg\n              width=\"100%\"\n              height=\"100%\"\n              viewBox={`0 0 ${baseWidth} ${baseWidth}`}\n              className=\"absolute inset-0 pointer-events-none\"\n            >\n              <path d={path} fill=\"none\" stroke={pathColor} strokeWidth={pathWidth / (scale ?? 1)} />\n            </svg>\n          )}\n\n          {items.map((item, index) => (\n            <OrbitItem\n              key={index}\n              item={item}\n              index={index}\n              totalItems={items.length}\n              path={path}\n              itemSize={itemSize}\n              rotation={rotation}\n              progress={progress}\n              fill={fill}\n            />\n          ))}\n        </div>\n      </div>\n\n      {centerContent && (\n        <div className=\"absolute inset-0 flex items-center justify-center z-10\">\n          {centerContent}\n        </div>\n      )}\n    </div>\n  );\n}\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"motion@^12.23.12"
	]
}