{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cambio-image",
  "dependencies": [
    "lucide-react",
    "cambio"
  ],
  "files": [
    {
      "path": "components/ui/cambio-image.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useRef, useState } from \"react\"\nimport { Cambio } from \"cambio\"\nimport { Maximize2, X } from \"lucide-react\"\nimport { createPortal } from \"react-dom\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction CloseIcon() {\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      className=\"size-4\"\n      aria-hidden=\"true\"\n    >\n      <path d=\"M18 6 6 18\" />\n      <path d=\"m6 6 12 12\" />\n    </svg>\n  )\n}\n\nconst closeButtonClasses = cn(\n  \"inline-flex size-9 items-center justify-center rounded-full\",\n  \"bg-secondary text-secondary-foreground shadow-xs transition-colors\",\n  \"hover:bg-secondary/80 active:bg-secondary/70\",\n  \"focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none\",\n  \"disabled:pointer-events-none disabled:opacity-50\",\n  \"cursor-pointer\"\n)\n\ntype MotionPreset = \"snappy\" | \"smooth\" | \"bouncy\" | \"reduced\"\n\ninterface CambioImageProps {\n  src: string\n  alt: string\n  width: number\n  height: number\n  loading?: \"lazy\" | \"eager\"\n  index?: number\n  motion?:\n    | MotionPreset\n    | {\n        trigger?: MotionPreset\n        popup?: MotionPreset\n        backdrop?: MotionPreset\n      }\n  dismissible?: boolean\n  className?: string\n  draggable?: boolean\n  enableInitialAnimation?: boolean\n  dismissOnImageClick?: boolean\n  dismissOnScroll?: boolean\n  showExpandIcon?: boolean\n  iconsOnlyMode?: boolean\n  /**\n   * Renders a close button when the image is focused. On desktop the button\n   * sits in the top-right of the image; on mobile it floats at the viewport's\n   * top-right (outside the image).\n   *\n   * When enabled, `draggable` is coerced to `false` so mobile users can pinch\n   * to zoom the focused image.\n   */\n  controls?: boolean\n}\n\nexport function CambioImage({\n  src,\n  alt,\n  width,\n  height,\n  loading = \"lazy\",\n  index = 0,\n  motion = \"snappy\",\n  dismissible = true,\n  className,\n  draggable = false,\n  enableInitialAnimation = true,\n  dismissOnImageClick = false,\n  dismissOnScroll = false,\n  showExpandIcon = false,\n  iconsOnlyMode = false,\n  controls = false,\n}: CambioImageProps) {\n  const effectiveDraggable = controls ? false : draggable\n  const [isVisible, setIsVisible] = useState(false)\n  const [open, setOpen] = useState(false)\n  const [isHovered, setIsHovered] = useState(false)\n  const [mounted, setMounted] = useState(false)\n  const ref = useRef<HTMLDivElement>(null)\n\n  useEffect(() => {\n    setMounted(true)\n  }, [])\n\n  useEffect(() => {\n    if (!enableInitialAnimation) {\n      setIsVisible(true)\n      return\n    }\n\n    const io = new IntersectionObserver(\n      ([e]) => {\n        if (e.isIntersecting) {\n          setIsVisible(true)\n          io.disconnect()\n        }\n      },\n      { threshold: 0.1 }\n    )\n    if (ref.current) {\n      io.observe(ref.current)\n    }\n    return () => io.disconnect()\n  }, [enableInitialAnimation])\n\n  useEffect(() => {\n    if (!dismissOnScroll || !open) return\n\n    let hasDismissed = false\n    let touchStartY = 0\n\n    const handleScroll = () => {\n      if (hasDismissed) return\n      hasDismissed = true\n      setOpen(false)\n    }\n\n    const handleTouchStart = (e: TouchEvent) => {\n      touchStartY = e.touches[0].clientY\n    }\n\n    const handleTouchMove = (e: TouchEvent) => {\n      if (hasDismissed) return\n      const touchCurrentY = e.touches[0].clientY\n      const deltaY = touchStartY - touchCurrentY\n\n      hasDismissed = true\n      setOpen(false)\n\n      requestAnimationFrame(() => {\n        window.scrollBy({ top: deltaY, behavior: \"instant\" })\n      })\n    }\n\n    window.addEventListener(\"scroll\", handleScroll, { passive: true })\n    window.addEventListener(\"wheel\", handleScroll, { passive: true })\n    window.addEventListener(\"touchstart\", handleTouchStart, { passive: true })\n    window.addEventListener(\"touchmove\", handleTouchMove, { passive: true })\n\n    return () => {\n      window.removeEventListener(\"scroll\", handleScroll)\n      window.removeEventListener(\"wheel\", handleScroll)\n      window.removeEventListener(\"touchstart\", handleTouchStart)\n      window.removeEventListener(\"touchmove\", handleTouchMove)\n    }\n  }, [dismissOnScroll, open])\n\n  const zIndex = open ? 50 : 10 + index\n\n  return (\n    <span\n      ref={ref}\n      className={cn(\n        \"relative inline-block w-full transition-all duration-500 ease-out\",\n        className\n      )}\n      style={{\n        opacity: enableInitialAnimation ? (isVisible ? 1 : 0) : 1,\n        filter: enableInitialAnimation\n          ? isVisible\n            ? \"blur(0)\"\n            : \"blur(4px)\"\n          : \"blur(0)\",\n        zIndex,\n      }}\n    >\n      {/* @ts-ignore */}\n      <Cambio.Root\n        motion={motion}\n        dismissible={iconsOnlyMode ? false : dismissible}\n        open={open}\n        onOpenChange={setOpen}\n      >\n        {/* @ts-ignore */}\n        <Cambio.Trigger\n          className={cn(\n            \"relative w-full overflow-hidden\",\n            !open && !showExpandIcon && \"cursor-zoom-in\"\n          )}\n          style={{ pointerEvents: open ? \"none\" : \"auto\" }}\n          onMouseEnter={() => setIsHovered(true)}\n          onMouseLeave={() => setIsHovered(false)}\n        >\n          <img\n            src={src || \"/placeholder.svg\"}\n            alt={alt}\n            width={width}\n            height={height}\n            loading={loading}\n            draggable={effectiveDraggable}\n            className={cn(\"h-auto w-full\", className)}\n            style={{ pointerEvents: \"none\" }}\n          />\n\n          {showExpandIcon && (\n            <div\n              className={cn(\n                \"absolute top-2 left-2 flex h-10 w-10 items-center justify-center rounded-full\",\n                \"border border-white/30 bg-white/25 p-2 backdrop-blur-lg transition-colors duration-150 ease-out hover:bg-white/35\",\n                \"dark:border-black/50 dark:bg-black/75 dark:hover:bg-black/85\",\n                \"cursor-pointer transition-all duration-200 ease-out md:scale-0 md:opacity-0\",\n                isHovered && \"md:scale-100 md:opacity-100\"\n              )}\n              onClick={() => setOpen(true)}\n            >\n              <Maximize2 className=\"size-4 stroke-white\" strokeWidth={2} />\n            </div>\n          )}\n        </Cambio.Trigger>\n\n        {/* @ts-ignore */}\n        <Cambio.Portal>\n          {/* @ts-ignore */}\n          <Cambio.Backdrop\n            motion=\"reduced\"\n            className=\"fixed inset-0 z-[100] bg-black/40\"\n          />\n          {/* @ts-ignore */}\n          <Cambio.Popup className=\"relative z-[101] flex w-full items-center justify-center overflow-hidden md:w-auto\">\n            <div className=\"relative flex max-h-[90vh] max-w-[90vw] items-center justify-center\">\n              {showExpandIcon && !controls && (\n                <button\n                  onClick={() => setOpen(false)}\n                  className={cn(\n                    \"absolute top-2 right-2 z-10 flex h-10 w-10 items-center justify-center rounded-full\",\n                    \"border border-white/30 bg-white/25 p-2 backdrop-blur-lg transition-colors duration-150 ease-out hover:bg-white/35\",\n                    \"dark:border-black/50 dark:bg-black/75 dark:hover:bg-black/85\",\n                    \"cursor-pointer focus:outline-hidden\"\n                  )}\n                  aria-label=\"Close expanded view\"\n                >\n                  <X className=\"size-4 stroke-white\" strokeWidth={2} />\n                </button>\n              )}\n\n              {controls && (\n                <button\n                  type=\"button\"\n                  onClick={() => setOpen(false)}\n                  aria-label=\"Close\"\n                  className={cn(\n                    closeButtonClasses,\n                    \"absolute top-2 right-2 z-10 hidden md:inline-flex\"\n                  )}\n                >\n                  <CloseIcon />\n                </button>\n              )}\n\n              <img\n                src={src || \"/placeholder.svg\"}\n                alt={alt}\n                width={width}\n                height={height}\n                loading=\"eager\"\n                draggable={effectiveDraggable}\n                className={cn(\n                  \"h-auto max-h-[90vh] w-full max-w-[90vw] object-contain\",\n                  !iconsOnlyMode && dismissOnImageClick && \"cursor-zoom-out\"\n                )}\n                style={{\n                  pointerEvents:\n                    !iconsOnlyMode && dismissOnImageClick ? \"auto\" : \"none\",\n                }}\n                onClick={\n                  !iconsOnlyMode && dismissOnImageClick\n                    ? () => setOpen(false)\n                    : undefined\n                }\n              />\n            </div>\n          </Cambio.Popup>\n        </Cambio.Portal>\n      </Cambio.Root>\n      {controls &&\n        open &&\n        mounted &&\n        createPortal(\n          <button\n            type=\"button\"\n            onClick={() => setOpen(false)}\n            aria-label=\"Close\"\n            className={cn(\n              closeButtonClasses,\n              \"fixed top-3 right-3 z-[9999] md:hidden\"\n            )}\n          >\n            <CloseIcon />\n          </button>,\n          document.body\n        )}\n    </span>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}