{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"morph-stack","title":"Morph Stack","author":"CoDesign-Spa27","description":"A composable three-plate stack with spring-driven perspective and layered hover motion.","dependencies":["motion"],"files":[{"path":"components/ui-components/morph-stack.tsx","content":"\"use client\";\n\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { type ComponentProps, type ReactNode, useState } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport type MorphStackSpring = {\n  bounce?: number;\n  damping?: number;\n  mass?: number;\n  stiffness?: number;\n  visualDuration?: number;\n};\n\nexport type MorphStackTransform = {\n  x?: number;\n  y?: number;\n  z?: number;\n};\n\nexport type MorphStackLayerMotion = {\n  active?: MorphStackTransform;\n  resting?: MorphStackTransform;\n};\n\nexport type MorphStackMotion = {\n  back?: MorphStackLayerMotion & {\n    activeOpacity?: number;\n    restingOpacity?: number;\n  };\n  delays?: {\n    back?: number;\n    front?: number;\n    middle?: number;\n  };\n  front?: MorphStackLayerMotion;\n  middle?: MorphStackLayerMotion;\n  perspective?: number;\n  reducedMotionStrength?: number;\n  springs?: {\n    back?: MorphStackSpring;\n    front?: MorphStackSpring;\n    middle?: MorphStackSpring;\n    stack?: MorphStackSpring;\n  };\n  stack?: {\n    rotateX?: number;\n    rotateZ?: number;\n    scale?: number;\n  };\n};\n\nexport type MorphStackProps = {\n  /** Any renderable plate, such as an image, inline SVG, or custom component. */\n  backPlate: ReactNode;\n  className?: string;\n  /** Controls the expanded state. Leave undefined to use fine-pointer hover. */\n  expanded?: boolean;\n  /** Any renderable plate, such as an image, inline SVG, or custom component. */\n  frontPlate: ReactNode;\n  interactive?: boolean;\n  /** Any renderable plate, such as an image, inline SVG, or custom component. */\n  middlePlate: ReactNode;\n  motion?: MorphStackMotion;\n  onExpandedChange?: (expanded: boolean) => void;\n};\n\ntype PlateLayerProps = ComponentProps<typeof motion.div> & {\n  layer: number;\n};\n\nconst DEFAULT_MOTION = {\n  perspective: 900,\n  reducedMotionStrength: 0.25,\n  stack: {\n    rotateX: 58,\n    rotateZ: -42,\n    scale: 1.06,\n  },\n  front: {\n    active: {\n      x: -14,\n      y: 20,\n      z: 72,\n    },\n    resting: {\n      x: 4,\n      y: 0,\n      z: 12,\n    },\n  },\n  middle: {\n    active: {\n      x: 0,\n      y: 0,\n      z: 0,\n    },\n    resting: {\n      x: 0,\n      y: 0,\n      z: 0,\n    },\n  },\n  back: {\n    active: {\n      x: 0,\n      y: 0,\n      z: -76,\n    },\n    activeOpacity: 1,\n    resting: {\n      x: 0,\n      y: 0,\n      z: -4,\n    },\n    restingOpacity: 0,\n  },\n  delays: {\n    back: 0.2,\n    front: 0.1,\n    middle: 0,\n  },\n} satisfies Required<Omit<MorphStackMotion, \"springs\">>;\n\nconst DEFAULT_SPRINGS = {\n  stack: {\n    type: \"spring\",\n    stiffness: 320,\n    damping: 20,\n    mass: 0.65,\n  },\n  front: {\n    type: \"spring\",\n    stiffness: 420,\n    damping: 24,\n    mass: 0.6,\n  },\n  middle: {\n    type: \"spring\",\n    stiffness: 360,\n    damping: 30,\n    mass: 0.65,\n  },\n  back: {\n    type: \"spring\",\n    stiffness: 300,\n    damping: 27,\n    mass: 0.7,\n  },\n} as const;\n\nfunction canFineHover() {\n  return window.matchMedia(\"(hover: hover) and (pointer: fine)\").matches;\n}\n\nfunction PlateLayer({ children, className, layer, style, ...props }: PlateLayerProps) {\n  return (\n    <motion.div\n      {...props}\n      className={cn(\"col-start-1 row-start-1\", className)}\n      style={{\n        ...style,\n        zIndex: layer,\n        transformStyle: \"preserve-3d\",\n        willChange: \"transform\",\n      }}\n    >\n      {children}\n    </motion.div>\n  );\n}\n\nfunction resolveSpring(defaultSpring: MorphStackSpring, spring: MorphStackSpring | undefined) {\n  if (spring?.visualDuration !== undefined || spring?.bounce !== undefined) {\n    return {\n      bounce: spring.bounce ?? 0,\n      type: \"spring\" as const,\n      visualDuration: spring.visualDuration ?? 0.3,\n    };\n  }\n\n  return {\n    ...defaultSpring,\n    ...spring,\n    type: \"spring\" as const,\n  };\n}\n\nfunction resolveLayerMotion(\n  defaults: {\n    active: Required<MorphStackTransform>;\n    resting: Required<MorphStackTransform>;\n  },\n  motion: MorphStackLayerMotion | undefined,\n) {\n  return {\n    active: {\n      x: motion?.active?.x ?? defaults.active.x,\n      y: motion?.active?.y ?? defaults.active.y,\n      z: motion?.active?.z ?? defaults.active.z,\n    },\n    resting: {\n      x: motion?.resting?.x ?? defaults.resting.x,\n      y: motion?.resting?.y ?? defaults.resting.y,\n      z: motion?.resting?.z ?? defaults.resting.z,\n    },\n  };\n}\n\nfunction clampOpacity(value: number) {\n  return Math.min(1, Math.max(0, value));\n}\n\nexport function MorphStack({\n  backPlate,\n  className,\n  expanded,\n  frontPlate,\n  interactive = true,\n  middlePlate,\n  motion: motionConfig,\n  onExpandedChange,\n}: MorphStackProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const [hovered, setHovered] = useState(false);\n  const isExpanded = expanded ?? (interactive && hovered);\n  const reducedMotionStrength = Math.min(\n    1,\n    Math.max(0, motionConfig?.reducedMotionStrength ?? DEFAULT_MOTION.reducedMotionStrength),\n  );\n  const strength = shouldReduceMotion ? reducedMotionStrength : 1;\n  const stack = { ...DEFAULT_MOTION.stack, ...motionConfig?.stack };\n  const front = resolveLayerMotion(DEFAULT_MOTION.front, motionConfig?.front);\n  const middle = resolveLayerMotion(DEFAULT_MOTION.middle, motionConfig?.middle);\n  const back = {\n    ...resolveLayerMotion(DEFAULT_MOTION.back, motionConfig?.back),\n    activeOpacity: clampOpacity(\n      motionConfig?.back?.activeOpacity ?? DEFAULT_MOTION.back.activeOpacity,\n    ),\n    restingOpacity: clampOpacity(\n      motionConfig?.back?.restingOpacity ?? DEFAULT_MOTION.back.restingOpacity,\n    ),\n  };\n  const delays = { ...DEFAULT_MOTION.delays, ...motionConfig?.delays };\n  const springs = {\n    back: resolveSpring(DEFAULT_SPRINGS.back, motionConfig?.springs?.back),\n    front: resolveSpring(DEFAULT_SPRINGS.front, motionConfig?.springs?.front),\n    middle: resolveSpring(DEFAULT_SPRINGS.middle, motionConfig?.springs?.middle),\n    stack: resolveSpring(DEFAULT_SPRINGS.stack, motionConfig?.springs?.stack),\n  };\n\n  function handlePointerEnter() {\n    if (!interactive || !canFineHover()) return;\n\n    setHovered(true);\n    onExpandedChange?.(true);\n  }\n\n  function handlePointerLeave() {\n    setHovered(false);\n    if (hovered) {\n      onExpandedChange?.(false);\n    }\n  }\n\n  return (\n    <motion.div\n      className={cn(\n        \"relative flex h-96 w-96 items-center justify-center\",\n        interactive && \"cursor-pointer\",\n        className,\n      )}\n      onPointerEnter={handlePointerEnter}\n      onPointerLeave={handlePointerLeave}\n      style={{ perspective: Math.max(1, motionConfig?.perspective ?? DEFAULT_MOTION.perspective) }}\n    >\n      <motion.div\n        animate={{\n          rotateX: isExpanded ? stack.rotateX * strength : 0,\n          rotateZ: isExpanded ? stack.rotateZ * strength : 0,\n          scale: isExpanded ? 1 + (stack.scale - 1) * strength : 1,\n        }}\n        className=\"relative grid place-items-center\"\n        style={{\n          transformOrigin: \"50% 50%\",\n          transformStyle: \"preserve-3d\",\n        }}\n        transition={springs.stack}\n      >\n        <PlateLayer\n          animate={{\n            opacity: isExpanded ? back.activeOpacity : back.restingOpacity,\n            x: isExpanded ? back.active.x * strength : back.resting.x,\n            y: isExpanded ? back.active.y * strength : back.resting.y,\n            z: isExpanded ? back.active.z * strength : back.resting.z,\n          }}\n          layer={1}\n          transition={{ ...springs.back, delay: isExpanded ? delays.back : 0 }}\n        >\n          {backPlate}\n        </PlateLayer>\n\n        <PlateLayer\n          animate={{\n            x: isExpanded ? middle.active.x * strength : middle.resting.x,\n            y: isExpanded ? middle.active.y * strength : middle.resting.y,\n            z: isExpanded ? middle.active.z * strength : middle.resting.z,\n          }}\n          layer={2}\n          transition={{ ...springs.middle, delay: isExpanded ? delays.middle : 0 }}\n        >\n          {middlePlate}\n        </PlateLayer>\n\n        <PlateLayer\n          animate={{\n            x: isExpanded ? front.active.x * strength : front.resting.x,\n            y: isExpanded ? front.active.y * strength : front.resting.y,\n            z: isExpanded ? front.active.z * strength : front.resting.z,\n          }}\n          layer={3}\n          transition={{ ...springs.front, delay: isExpanded ? delays.front : 0 }}\n        >\n          {frontPlate}\n        </PlateLayer>\n      </motion.div>\n    </motion.div>\n  );\n}\n\nexport default MorphStack;\n","type":"registry:ui"}],"categories":["interaction","animation"],"type":"registry:ui"}