{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A MotionLoader component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport type { CSSProperties, ReactElement } from \"react\";\n\n/* -------------------------------------------------------------------------- */\n/* Public types                                                               */\n/* -------------------------------------------------------------------------- */\n\nexport const MOTION_LOADER_VARIANTS = [\n  \"orbit\",\n  \"newton-cradle\",\n  \"pendulum\",\n  \"hourglass\",\n  \"morph-ring\",\n  \"square-snake\",\n  \"comet\",\n  \"radar\",\n  \"cube-flip\",\n  \"wave-bars\",\n  \"breathing-glow\",\n  \"dot-ring\",\n] as const;\n\nexport type MotionLoaderVariant = (typeof MOTION_LOADER_VARIANTS)[number];\n\nexport interface MotionLoaderProps {\n  className?: string;\n  /** Any CSS colour. Defaults to `currentColor`, so it inherits text colour. */\n  color?: string;\n  /** Accessible label announced by screen readers. */\n  label?: string;\n  /** Square size in pixels. Every variant scales from this value. */\n  size?: number;\n  /** Animation speed multiplier. `2` runs twice as fast. */\n  speed?: number;\n  variant?: MotionLoaderVariant;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Constants                                                                  */\n/* -------------------------------------------------------------------------- */\n\nconst DEFAULT_SIZE = 40;\nconst DEFAULT_SPEED = 1;\nconst DEFAULT_LABEL = \"Loading\";\nconst DEFAULT_VARIANT: MotionLoaderVariant = \"orbit\";\n\nconst LINEAR: [number, number, number, number] = [0, 0, 1, 1];\nconst EASE_IN_OUT: [number, number, number, number] = [0.645, 0.045, 0.355, 1];\nconst EASE_OUT: [number, number, number, number] = [0.23, 1, 0.32, 1];\n\nconst CYCLE = 1.1;\nconst SLOW_CYCLE = 1.4;\nconst FULL_TURN = 360;\nconst HALF_TURN = 180;\n\nconst DOT_RATIO = 0.2;\nconst SMALL_DOT_RATIO = 0.14;\nconst TRACK_OPACITY = 0.2;\nconst TRAIL_LENGTH = 4;\nconst TRAIL_STEP_DEG = 16;\nconst CRADLE_BALLS = 5;\nconst CRADLE_SWING_DEG = 34;\nconst PENDULUM_SWING_DEG = 32;\nconst WAVE_BARS = 5;\nconst WAVE_MIN_SCALE = 0.3;\nconst WAVE_STAGGER = 0.1;\nconst RING_DOTS = 8;\nconst RING_DOT_FADE = 0.15;\nconst RING_STAGGER = 0.09;\nconst SNAKE_CELL_RATIO = 0.38;\nconst SNAKE_CORNERS = 4;\nconst GLOW_SCALE = 1.25;\nconst GLOW_MIN_OPACITY = 0.45;\nconst HALF = 0.5;\n\nconst loop = (duration: number, ease: [number, number, number, number]) => ({\n  duration,\n  ease,\n  repeat: Number.POSITIVE_INFINITY,\n  repeatType: \"loop\" as const,\n});\n\nconst delayedLoop = (\n  duration: number,\n  ease: [number, number, number, number],\n  delay: number\n) => ({ ...loop(duration, ease), delay });\n\ninterface LoaderVariantProps {\n  reduce: boolean;\n  size: number;\n  speed: number;\n}\n\nconst dotStyle = (diameter: number): CSSProperties => ({\n  height: `${diameter}px`,\n  width: `${diameter}px`,\n});\n\n/** Stable React keys for the fixed-length element lists of each variant. */\nconst idList = (prefix: string, count: number): string[] =>\n  Array.from({ length: count }, (_, index) => `${prefix}-${index}`);\n\nconst CRADLE_IDS = idList(\"cradle\", CRADLE_BALLS);\nconst SNAKE_IDS = idList(\"snake\", SNAKE_CORNERS);\nconst COMET_IDS = idList(\"comet\", TRAIL_LENGTH);\nconst WAVE_IDS = idList(\"wave\", WAVE_BARS);\nconst RING_IDS = idList(\"ring\", RING_DOTS);\n\n/* -------------------------------------------------------------------------- */\n/* Variants                                                                   */\n/* -------------------------------------------------------------------------- */\n\nconst OrbitLoader = ({ reduce, size, speed }: LoaderVariantProps) => {\n  const dot = size * DOT_RATIO;\n  return (\n    <>\n      <span\n        className=\"absolute inset-0 rounded-full border border-current\"\n        style={{ opacity: TRACK_OPACITY }}\n      />\n      <motion.span\n        animate={reduce ? undefined : { rotate: FULL_TURN }}\n        className=\"absolute inset-0\"\n        transition={reduce ? undefined : loop(CYCLE / speed, LINEAR)}\n      >\n        <span\n          className=\"absolute top-0 left-1/2 rounded-full bg-current\"\n          style={{\n            ...dotStyle(dot),\n            marginLeft: -dot * HALF,\n            marginTop: -dot * HALF,\n          }}\n        />\n      </motion.span>\n    </>\n  );\n};\n\nconst CRADLE_TIMES = [0, 0.25, 0.5, 0.75, 1];\n\nconst cradleKeyframes = (index: number): number[] | undefined => {\n  if (index === 0) {\n    return [0, -CRADLE_SWING_DEG, 0, 0, 0];\n  }\n  if (index === CRADLE_BALLS - 1) {\n    return [0, 0, 0, CRADLE_SWING_DEG, 0];\n  }\n};\n\nconst NewtonCradleLoader = ({ reduce, size, speed }: LoaderVariantProps) => {\n  const ball = size / CRADLE_BALLS;\n  return (\n    <span className=\"absolute inset-0 flex items-start justify-center\">\n      {CRADLE_IDS.map((id, index) => {\n        const keyframes = cradleKeyframes(index);\n        return (\n          <motion.span\n            animate={reduce || !keyframes ? undefined : { rotate: keyframes }}\n            className=\"relative block\"\n            key={id}\n            style={{\n              height: `${size}px`,\n              transformOrigin: \"top center\",\n              width: `${ball}px`,\n            }}\n            transition={\n              reduce || !keyframes\n                ? undefined\n                : {\n                    ...loop(SLOW_CYCLE / speed, EASE_IN_OUT),\n                    times: CRADLE_TIMES,\n                  }\n            }\n          >\n            <span\n              className=\"absolute top-0 bottom-0 left-1/2 w-px bg-current\"\n              style={{ opacity: TRACK_OPACITY }}\n            />\n            <span\n              className=\"absolute bottom-0 left-1/2 rounded-full bg-current\"\n              style={{ ...dotStyle(ball), marginLeft: -ball * HALF }}\n            />\n          </motion.span>\n        );\n      })}\n    </span>\n  );\n};\n\nconst PendulumLoader = ({ reduce, size, speed }: LoaderVariantProps) => {\n  const bob = size * DOT_RATIO * 1.5;\n  return (\n    <motion.span\n      animate={\n        reduce\n          ? undefined\n          : {\n              rotate: [\n                -PENDULUM_SWING_DEG,\n                PENDULUM_SWING_DEG,\n                -PENDULUM_SWING_DEG,\n              ],\n            }\n      }\n      className=\"absolute inset-0\"\n      style={{ transformOrigin: \"top center\" }}\n      transition={reduce ? undefined : loop(SLOW_CYCLE / speed, EASE_IN_OUT)}\n    >\n      <span\n        className=\"absolute top-0 bottom-2 left-1/2 w-px bg-current\"\n        style={{ opacity: TRACK_OPACITY }}\n      />\n      <span\n        className=\"absolute bottom-0 left-1/2 rounded-full bg-current\"\n        style={{ ...dotStyle(bob), marginLeft: -bob * HALF }}\n      />\n    </motion.span>\n  );\n};\n\nconst HourglassLoader = ({ reduce, size, speed }: LoaderVariantProps) => (\n  <motion.svg\n    animate={\n      reduce ? undefined : { rotate: [0, 0, HALF_TURN, HALF_TURN, FULL_TURN] }\n    }\n    className=\"absolute inset-0 fill-current\"\n    height={size}\n    transition={\n      reduce\n        ? undefined\n        : {\n            ...loop(SLOW_CYCLE / speed, EASE_IN_OUT),\n            times: [0, 0.3, 0.5, 0.8, 1],\n          }\n    }\n    viewBox=\"0 0 24 24\"\n    width={size}\n  >\n    <path d=\"M5 2h14v2l-6 8 6 8v2H5v-2l6-8-6-8V2z\" opacity={TRACK_OPACITY} />\n    <path d=\"M7 4h10l-5 6.5L7 4zm0 16h10l-5-6.5L7 20z\" />\n  </motion.svg>\n);\n\nconst MorphRingLoader = ({ reduce, size, speed }: LoaderVariantProps) => (\n  <motion.svg\n    animate={reduce ? undefined : { scale: [1, 0.88, 1] }}\n    className=\"absolute inset-0 stroke-current\"\n    fill=\"none\"\n    height={size}\n    transition={reduce ? undefined : loop(CYCLE / speed, EASE_IN_OUT)}\n    viewBox=\"0 0 48 48\"\n    width={size}\n  >\n    <motion.circle\n      animate={reduce ? undefined : { rotate: FULL_TURN }}\n      cx=\"24\"\n      cy=\"24\"\n      r=\"20\"\n      strokeDasharray=\"62 64\"\n      strokeLinecap=\"round\"\n      strokeWidth=\"4\"\n      style={{ transformOrigin: \"center\" }}\n      transition={reduce ? undefined : loop(CYCLE / speed, LINEAR)}\n    />\n    <motion.circle\n      animate={reduce ? undefined : { rotate: -FULL_TURN }}\n      cx=\"24\"\n      cy=\"24\"\n      opacity={0.45}\n      r=\"11\"\n      strokeDasharray=\"24 45\"\n      strokeLinecap=\"round\"\n      strokeWidth=\"4\"\n      style={{ transformOrigin: \"center\" }}\n      transition={reduce ? undefined : loop((CYCLE * 1.5) / speed, LINEAR)}\n    />\n  </motion.svg>\n);\n\nconst SquareSnakeLoader = ({ reduce, size, speed }: LoaderVariantProps) => {\n  const cell = size * SNAKE_CELL_RATIO;\n  const travel = size - cell;\n  const positions: CSSProperties[] = [\n    { left: 0, top: 0 },\n    { left: travel, top: 0 },\n    { left: travel, top: travel },\n    { left: 0, top: travel },\n  ];\n  return (\n    <>\n      {SNAKE_IDS.map((id, index) => (\n        <span\n          className=\"absolute rounded-sm bg-current\"\n          key={id}\n          style={{\n            ...positions[index],\n            ...dotStyle(cell),\n            opacity: TRACK_OPACITY,\n          }}\n        />\n      ))}\n      <motion.span\n        animate={\n          reduce\n            ? undefined\n            : { x: [0, travel, travel, 0, 0], y: [0, 0, travel, travel, 0] }\n        }\n        className=\"absolute top-0 left-0 rounded-sm bg-current\"\n        style={dotStyle(cell)}\n        transition={\n          reduce\n            ? undefined\n            : {\n                ...loop(SLOW_CYCLE / speed, EASE_IN_OUT),\n                times: [0, 0.25, 0.5, 0.75, 1],\n              }\n        }\n      />\n    </>\n  );\n};\n\nconst CometLoader = ({ reduce, size, speed }: LoaderVariantProps) => {\n  const dot = size * SMALL_DOT_RATIO;\n  return (\n    <motion.span\n      animate={reduce ? undefined : { rotate: FULL_TURN }}\n      className=\"absolute inset-0\"\n      transition={reduce ? undefined : loop(CYCLE / speed, LINEAR)}\n    >\n      {COMET_IDS.map((id, index) => (\n        <span\n          className=\"absolute inset-0\"\n          key={id}\n          style={{ transform: `rotate(${-index * TRAIL_STEP_DEG}deg)` }}\n        >\n          <span\n            className=\"absolute top-0 left-1/2 rounded-full bg-current\"\n            style={{\n              ...dotStyle(dot * (1 - index / (TRAIL_LENGTH * 2))),\n              marginLeft: -dot * HALF,\n              opacity: 1 - index / TRAIL_LENGTH,\n            }}\n          />\n        </span>\n      ))}\n    </motion.span>\n  );\n};\n\nconst RadarLoader = ({ reduce, size, speed }: LoaderVariantProps) => {\n  const dot = size * SMALL_DOT_RATIO;\n  return (\n    <>\n      <span\n        className=\"absolute inset-0 rounded-full border border-current\"\n        style={{ opacity: TRACK_OPACITY }}\n      />\n      <span className=\"absolute inset-0 overflow-hidden rounded-full\">\n        <motion.span\n          animate={reduce ? undefined : { rotate: FULL_TURN }}\n          className=\"absolute inset-0\"\n          style={{\n            background:\n              \"conic-gradient(from 0deg, transparent 0deg, transparent 260deg, currentColor 360deg)\",\n          }}\n          transition={reduce ? undefined : loop(CYCLE / speed, LINEAR)}\n        />\n      </span>\n      <span\n        className=\"absolute top-1/2 left-1/2 rounded-full bg-current\"\n        style={{\n          ...dotStyle(dot),\n          marginLeft: -dot * HALF,\n          marginTop: -dot * HALF,\n        }}\n      />\n    </>\n  );\n};\n\nconst CubeFlipLoader = ({ reduce, size, speed }: LoaderVariantProps) => (\n  <span className=\"absolute inset-0\" style={{ perspective: `${size * 3}px` }}>\n    <motion.span\n      animate={\n        reduce\n          ? undefined\n          : {\n              rotateX: [0, HALF_TURN, HALF_TURN, FULL_TURN],\n              rotateY: [0, 0, HALF_TURN, HALF_TURN],\n            }\n      }\n      className=\"absolute inset-0 rounded-md bg-current\"\n      transition={\n        reduce\n          ? undefined\n          : {\n              ...loop((SLOW_CYCLE * 1.5) / speed, EASE_IN_OUT),\n              times: [0, 0.33, 0.66, 1],\n            }\n      }\n    />\n  </span>\n);\n\nconst WaveBarsLoader = ({ reduce, size, speed }: LoaderVariantProps) => {\n  const barWidth = size / (WAVE_BARS * 2);\n  return (\n    <span\n      className=\"absolute inset-0 flex items-center justify-between\"\n      style={{ gap: `${barWidth * HALF}px` }}\n    >\n      {WAVE_IDS.map((id, index) => (\n        <motion.span\n          animate={\n            reduce ? undefined : { scaleY: [WAVE_MIN_SCALE, 1, WAVE_MIN_SCALE] }\n          }\n          className=\"flex-1 rounded-full bg-current\"\n          key={id}\n          style={{\n            height: `${size}px`,\n            scaleY: reduce ? WAVE_MIN_SCALE + index * WAVE_STAGGER : undefined,\n          }}\n          transition={\n            reduce\n              ? undefined\n              : delayedLoop(CYCLE / speed, EASE_IN_OUT, index * WAVE_STAGGER)\n          }\n        />\n      ))}\n    </span>\n  );\n};\n\nconst BreathingGlowLoader = ({ reduce, size, speed }: LoaderVariantProps) => {\n  const core = size * HALF;\n  return (\n    <>\n      <motion.span\n        animate={\n          reduce\n            ? undefined\n            : {\n                opacity: [GLOW_MIN_OPACITY, 0, GLOW_MIN_OPACITY],\n                scale: [1, GLOW_SCALE, 1],\n              }\n        }\n        className=\"absolute inset-0 rounded-full bg-current blur-md\"\n        style={{ opacity: GLOW_MIN_OPACITY }}\n        transition={reduce ? undefined : loop(SLOW_CYCLE / speed, EASE_IN_OUT)}\n      />\n      <motion.span\n        animate={reduce ? undefined : { scale: [1, GLOW_SCALE, 1] }}\n        className=\"rounded-full bg-current\"\n        style={dotStyle(core)}\n        transition={reduce ? undefined : loop(SLOW_CYCLE / speed, EASE_OUT)}\n      />\n    </>\n  );\n};\n\nconst DotRingLoader = ({ reduce, size, speed }: LoaderVariantProps) => {\n  const dot = size * SMALL_DOT_RATIO;\n  const radius = size * HALF - dot * HALF;\n  return (\n    <>\n      {RING_IDS.map((id, index) => (\n        <motion.span\n          animate={reduce ? undefined : { opacity: [1, RING_DOT_FADE] }}\n          className=\"absolute top-1/2 left-1/2 rounded-full bg-current\"\n          key={id}\n          style={{\n            ...dotStyle(dot),\n            marginLeft: -dot * HALF,\n            marginTop: -dot * HALF,\n            opacity: reduce ? 1 - index * RING_DOT_FADE * HALF : undefined,\n            transform: `rotate(${(index * FULL_TURN) / RING_DOTS}deg) translateY(${-radius}px)`,\n          }}\n          transition={\n            reduce\n              ? undefined\n              : delayedLoop(CYCLE / speed, LINEAR, index * RING_STAGGER)\n          }\n        />\n      ))}\n    </>\n  );\n};\n\nconst VARIANT_RENDERERS: Record<\n  MotionLoaderVariant,\n  (props: LoaderVariantProps) => ReactElement\n> = {\n  \"breathing-glow\": BreathingGlowLoader,\n  comet: CometLoader,\n  \"cube-flip\": CubeFlipLoader,\n  \"dot-ring\": DotRingLoader,\n  hourglass: HourglassLoader,\n  \"morph-ring\": MorphRingLoader,\n  \"newton-cradle\": NewtonCradleLoader,\n  orbit: OrbitLoader,\n  pendulum: PendulumLoader,\n  radar: RadarLoader,\n  \"square-snake\": SquareSnakeLoader,\n  \"wave-bars\": WaveBarsLoader,\n};\n\n/* -------------------------------------------------------------------------- */\n/* Component                                                                  */\n/* -------------------------------------------------------------------------- */\n\nconst MotionLoader = ({\n  className,\n  color,\n  label = DEFAULT_LABEL,\n  size = DEFAULT_SIZE,\n  speed = DEFAULT_SPEED,\n  variant = DEFAULT_VARIANT,\n}: MotionLoaderProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const Renderer = VARIANT_RENDERERS[variant] ?? VARIANT_RENDERERS.orbit;\n  const safeSpeed = speed > 0 ? speed : DEFAULT_SPEED;\n\n  return (\n    <span\n      aria-label={label}\n      aria-live=\"polite\"\n      className={cn(\n        \"relative inline-flex shrink-0 items-center justify-center\",\n        className\n      )}\n      role=\"status\"\n      style={{\n        color: color ?? \"currentColor\",\n        height: `${size}px`,\n        width: `${size}px`,\n      }}\n    >\n      <Renderer\n        reduce={Boolean(shouldReduceMotion)}\n        size={size}\n        speed={safeSpeed}\n      />\n      <span className=\"sr-only\">{label}</span>\n    </span>\n  );\n};\n\nexport default MotionLoader;\n","path":"index.tsx","target":"components/smoothui/motion-loader/index.tsx","type":"registry:ui"}],"name":"motion-loader","registryDependencies":[],"title":"Motion Loader","type":"registry:ui"}