{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "morph-surface",
  "type": "registry:ui",
  "title": "Morph Surface",
  "description": "An unstyled layout primitive that grows, shrinks, clips, and updates the content of one persistent consumer-styled surface.",
  "files": [
    {
      "path": "registry/sonaui/morph-surface/morph-surface.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport {\n  animate,\n  type HTMLMotionProps,\n  motion,\n  type Transition,\n  useAnimationControls,\n  useMotionValue,\n  useReducedMotion,\n  useTransform,\n} from \"motion/react\";\nimport {\n  forwardRef,\n  type ReactNode,\n  useEffect,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport useMeasure from \"react-use-measure\";\n\nimport { motionTransition } from \"@/lib/sona-motion\";\nimport { cn } from \"@/lib/sona-utils\";\n\nexport type MorphSurfaceReducedMotion = \"user\" | \"always\" | \"never\";\nexport type MorphSurfaceOrigin =\n  | \"center\"\n  | \"top\"\n  | \"right\"\n  | \"bottom\"\n  | \"left\"\n  | \"top-left\"\n  | \"top-right\"\n  | \"bottom-right\"\n  | \"bottom-left\";\n\nconst morphSurfaceOrigins: Record<\n  MorphSurfaceOrigin,\n  { originX: number; originY: number }\n> = {\n  center: { originX: 0.5, originY: 0.5 },\n  top: { originX: 0.5, originY: 0 },\n  right: { originX: 1, originY: 0.5 },\n  bottom: { originX: 0.5, originY: 1 },\n  left: { originX: 0, originY: 0.5 },\n  \"top-left\": { originX: 0, originY: 0 },\n  \"top-right\": { originX: 1, originY: 0 },\n  \"bottom-right\": { originX: 1, originY: 1 },\n  \"bottom-left\": { originX: 0, originY: 1 },\n};\n\ntype SurfaceSize = { width: number; height: number };\n\nexport interface MorphSurfaceRootProps\n  extends Omit<HTMLMotionProps<\"div\">, \"children\" | \"transition\"> {\n  /** A value that identifies the current content state and starts a new morph when it changes. */\n  value: string;\n  /** The current content rendered inside the persistent surface. */\n  children: ReactNode;\n  /** The transition used when the persistent surface changes size. @default motionTransition.spatial */\n  transition?: Transition;\n  /** The transition used when the content changes. @default motionTransition.enter */\n  contentTransition?: Transition;\n  /** The scale from which changed content resolves inside the surface. @default 0.96 */\n  contentScale?: number;\n  /** The shared origin used to grow, shrink, reveal, and scale the surface content. @default \"center\" */\n  origin?: MorphSurfaceOrigin;\n  /** Additional classes for the stable layout anchor around the surface. @default undefined */\n  anchorClassName?: string;\n  /** Additional classes for the internal clipping layer. @default undefined */\n  clipClassName?: string;\n  /** Additional classes for the persistent content wrapper. @default undefined */\n  contentClassName?: string;\n  /** Controls whether the surface follows, forces, or ignores reduced motion. @default \"user\" */\n  reducedMotion?: MorphSurfaceReducedMotion;\n}\n\nconst MorphSurfaceRoot = forwardRef<HTMLDivElement, MorphSurfaceRootProps>(\n  function MorphSurfaceRoot(\n    {\n      value,\n      children,\n      className,\n      transition = motionTransition.spatial,\n      contentTransition = motionTransition.enter,\n      contentScale = 0.96,\n      origin = \"center\",\n      anchorClassName,\n      clipClassName,\n      contentClassName,\n      reducedMotion = \"user\",\n      style,\n      ...props\n    },\n    ref,\n  ) {\n    const [measureRef, bounds] = useMeasure({ offsetSize: true });\n    const [anchorSize, setAnchorSize] = useState<SurfaceSize | null>(null);\n    const renderedSize = useRef<SurfaceSize | null>(null);\n    const surfaceScaleX = useMotionValue(1);\n    const surfaceScaleY = useMotionValue(1);\n    const inverseSurfaceScaleX = useTransform(\n      surfaceScaleX,\n      (scale) => 1 / scale,\n    );\n    const inverseSurfaceScaleY = useTransform(\n      surfaceScaleY,\n      (scale) => 1 / scale,\n    );\n    const transitionRef = useRef(transition);\n    const previousValue = useRef(value);\n    const contentControls = useAnimationControls();\n    const userPrefersReducedMotion = useReducedMotion();\n    const shouldReduceMotion =\n      reducedMotion === \"always\" ||\n      (reducedMotion === \"user\" && userPrefersReducedMotion === true);\n    const { originX, originY } = morphSurfaceOrigins[origin];\n    const width = bounds.width || undefined;\n    const height = bounds.height || undefined;\n\n    transitionRef.current = transition;\n\n    useLayoutEffect(() => {\n      if (anchorSize || !bounds.width || !bounds.height) return;\n      setAnchorSize({ width: bounds.width, height: bounds.height });\n    }, [anchorSize, bounds.height, bounds.width]);\n\n    useLayoutEffect(() => {\n      if (!bounds.width || !bounds.height) return;\n\n      const nextSize = { width: bounds.width, height: bounds.height };\n      const previousSize = renderedSize.current;\n\n      if (!previousSize || shouldReduceMotion) {\n        renderedSize.current = nextSize;\n        surfaceScaleX.set(1);\n        surfaceScaleY.set(1);\n        return;\n      }\n\n      const currentWidth = previousSize.width * surfaceScaleX.get();\n      const currentHeight = previousSize.height * surfaceScaleY.get();\n      const nextScaleX = currentWidth / nextSize.width;\n      const nextScaleY = currentHeight / nextSize.height;\n\n      renderedSize.current = nextSize;\n      surfaceScaleX.set(nextScaleX);\n      surfaceScaleY.set(nextScaleY);\n\n      const surfaceXAnimation = animate(\n        surfaceScaleX,\n        1,\n        transitionRef.current,\n      );\n      const surfaceYAnimation = animate(\n        surfaceScaleY,\n        1,\n        transitionRef.current,\n      );\n\n      return () => {\n        surfaceXAnimation.stop();\n        surfaceYAnimation.stop();\n      };\n    }, [\n      bounds.height,\n      bounds.width,\n      shouldReduceMotion,\n      surfaceScaleX,\n      surfaceScaleY,\n    ]);\n\n    useEffect(() => {\n      if (previousValue.current === value) return;\n      previousValue.current = value;\n\n      if (shouldReduceMotion) {\n        contentControls.set({ opacity: 1, scale: 1 });\n        return;\n      }\n\n      contentControls.set({ opacity: 0, scale: contentScale });\n      void contentControls.start({ opacity: 1, scale: 1 }, contentTransition);\n    }, [\n      contentControls,\n      contentScale,\n      contentTransition,\n      shouldReduceMotion,\n      value,\n    ]);\n\n    return (\n      <div\n        data-slot=\"morph-surface-anchor\"\n        className={cn(\"relative inline-block align-top\", anchorClassName)}\n        style={{\n          width: anchorSize?.width ?? width,\n          height: anchorSize?.height ?? height,\n        }}\n      >\n        <motion.div\n          ref={ref}\n          data-slot=\"morph-surface\"\n          data-state={value}\n          data-reduced-motion={shouldReduceMotion ? \"true\" : \"false\"}\n          className={cn(\"absolute max-w-none\", className)}\n          initial={false}\n          style={{\n            ...style,\n            width,\n            height,\n            left: `${originX * 100}%`,\n            top: `${originY * 100}%`,\n            x: `${originX * -100}%`,\n            y: `${originY * -100}%`,\n            scaleX: surfaceScaleX,\n            scaleY: surfaceScaleY,\n            originX,\n            originY,\n            willChange: \"transform\",\n          }}\n          {...props}\n        >\n          <div\n            data-slot=\"morph-surface-clip\"\n            className={cn(\n              \"absolute inset-0 overflow-hidden rounded-[inherit]\",\n              clipClassName,\n            )}\n          >\n            <motion.div\n              data-slot=\"morph-surface-content-scale\"\n              className=\"absolute w-max max-w-none\"\n              style={{\n                left: `${originX * 100}%`,\n                top: `${originY * 100}%`,\n                x: `${originX * -100}%`,\n                y: `${originY * -100}%`,\n                scaleX: inverseSurfaceScaleX,\n                scaleY: inverseSurfaceScaleY,\n                originX,\n                originY,\n                willChange: \"transform\",\n              }}\n            >\n              <motion.div\n                ref={measureRef}\n                data-slot=\"morph-surface-content\"\n                animate={contentControls}\n                initial={false}\n                className={cn(\"w-max max-w-none\", contentClassName)}\n              >\n                {children}\n              </motion.div>\n            </motion.div>\n          </div>\n        </motion.div>\n      </div>\n    );\n  },\n);\n\nconst MorphSurface = {\n  Root: MorphSurfaceRoot,\n};\n\nexport { MorphSurfaceRoot };\nexport default MorphSurface;\n",
      "target": "components/ui/morph-surface/morph-surface.tsx"
    }
  ],
  "dependencies": [
    "motion",
    "react-use-measure"
  ],
  "registryDependencies": [
    "@sona-ui/sona-motion",
    "@sona-ui/sona-utils"
  ]
}