{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "lightbox",
  "type": "registry:ui",
  "title": "Lightbox",
  "description": "An accessible image preview that expands from its thumbnail and returns to the same spatial origin when dismissed.",
  "files": [
    {
      "path": "registry/sonaui/lightbox/lightbox.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { Dialog } from \"@base-ui/react/dialog\";\nimport {\n  AnimatePresence,\n  LayoutGroup,\n  motion,\n  useReducedMotion,\n} from \"motion/react\";\nimport { useId, useState } from \"react\";\n\nimport { cn } from \"@/lib/sona-utils\";\n\nexport interface LightboxProps {\n  /** The image URL shown in both the thumbnail and expanded preview. */\n  src: string;\n  /** Accessible alternative text for the image. */\n  alt: string;\n  /** Optional caption displayed beneath the expanded image. @default undefined */\n  caption?: string;\n  /** Controlled open state. @default undefined */\n  open?: boolean;\n  /** Initial open state for uncontrolled usage. @default false */\n  defaultOpen?: boolean;\n  /** Called whenever the expanded preview opens or closes. @default undefined */\n  onOpenChange?: (open: boolean) => void;\n  /** Additional CSS classes for the thumbnail trigger. @default undefined */\n  className?: string;\n  /** Additional CSS classes for the thumbnail image. @default undefined */\n  imageClassName?: string;\n  /** Additional CSS classes for the expanded image. @default undefined */\n  previewClassName?: string;\n  /** Additional CSS classes for the modal backdrop. @default undefined */\n  backdropClassName?: string;\n}\n\nexport default function Lightbox({\n  src,\n  alt,\n  caption,\n  open: controlledOpen,\n  defaultOpen = false,\n  onOpenChange,\n  className,\n  imageClassName,\n  previewClassName,\n  backdropClassName,\n}: LightboxProps) {\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const instanceId = useId();\n  const shouldReduceMotion = useReducedMotion();\n  const isControlled = controlledOpen !== undefined;\n  const open = isControlled ? controlledOpen : internalOpen;\n  const layoutId = shouldReduceMotion\n    ? undefined\n    : `${instanceId}-lightbox-image`;\n\n  const setOpen = (nextOpen: boolean) => {\n    if (!isControlled) setInternalOpen(nextOpen);\n    onOpenChange?.(nextOpen);\n  };\n\n  return (\n    <LayoutGroup id={instanceId}>\n      <Dialog.Root defaultOpen={defaultOpen} onOpenChange={setOpen} open={open}>\n        <Dialog.Trigger\n          className={cn(\n            \"group relative block w-full cursor-zoom-in rounded-2xl bg-muted text-left\",\n            \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground/35 focus-visible:ring-offset-4 focus-visible:ring-offset-background\",\n            className,\n          )}\n          render={\n            <motion.button\n              whileTap={shouldReduceMotion ? undefined : { scale: 0.985 }}\n            />\n          }\n        >\n          {/* biome-ignore lint/performance/noImgElement: a registry component must remain framework-agnostic */}\n          <motion.img\n            alt={alt}\n            className={cn(\n              \"block h-full w-full rounded-2xl object-cover\",\n              imageClassName,\n            )}\n            layoutId={layoutId}\n            src={src}\n            transition={{ type: \"spring\", bounce: 0, duration: 0.38 }}\n          />\n          <span className=\"absolute inset-0 rounded-2xl bg-gradient-to-t from-black/28 via-transparent to-transparent opacity-0 transition-opacity duration-200 ease-out group-hover:opacity-100\" />\n          <span className=\"absolute right-3 bottom-3 inline-flex size-9 translate-y-1 items-center justify-center rounded-full bg-black/52 text-white opacity-0 shadow-sm backdrop-blur-md transition-[opacity,transform] duration-200 ease-out group-hover:translate-y-0 group-hover:opacity-100 group-focus-visible:translate-y-0 group-focus-visible:opacity-100\">\n            <svg\n              aria-hidden=\"true\"\n              className=\"size-4\"\n              fill=\"none\"\n              viewBox=\"0 0 24 24\"\n            >\n              <path\n                d=\"m21 21-4.35-4.35M11 8v6m-3-3h6m5 0a8 8 0 1 1-16 0 8 8 0 0 1 16 0Z\"\n                stroke=\"currentColor\"\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n                strokeWidth=\"1.8\"\n              />\n            </svg>\n          </span>\n          <span className=\"sr-only\">Open image preview: {alt}</span>\n        </Dialog.Trigger>\n\n        <AnimatePresence>\n          {open ? (\n            <Dialog.Portal keepMounted>\n              <Dialog.Backdrop\n                className={cn(\n                  \"fixed inset-0 z-50 bg-black/72 backdrop-blur-md\",\n                  backdropClassName,\n                )}\n                render={\n                  <motion.div\n                    animate={{ opacity: 1 }}\n                    exit={{ opacity: 0 }}\n                    initial={{ opacity: 0 }}\n                    transition={{ duration: shouldReduceMotion ? 0.12 : 0.22 }}\n                  />\n                }\n              />\n\n              <Dialog.Popup\n                aria-label={`Image preview: ${alt}`}\n                className=\"fixed inset-0 z-50 grid cursor-zoom-out place-items-center p-5 sm:p-10\"\n                onClick={() => setOpen(false)}\n                render={<motion.div />}\n              >\n                <Dialog.Close\n                  aria-label=\"Close image preview\"\n                  className=\"absolute top-4 right-4 z-10 inline-flex size-10 cursor-pointer items-center justify-center rounded-full bg-black/45 text-white shadow-lg backdrop-blur-md transition-colors duration-150 ease-out hover:bg-black/65 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70 sm:top-6 sm:right-6\"\n                  onClick={(event) => event.stopPropagation()}\n                >\n                  <svg\n                    aria-hidden=\"true\"\n                    className=\"size-[18px]\"\n                    fill=\"none\"\n                    viewBox=\"0 0 24 24\"\n                  >\n                    <path\n                      d=\"M18 6 6 18M6 6l12 12\"\n                      stroke=\"currentColor\"\n                      strokeLinecap=\"round\"\n                      strokeWidth=\"2\"\n                    />\n                  </svg>\n                </Dialog.Close>\n\n                <motion.figure\n                  animate={{ opacity: 1 }}\n                  className=\"m-0 flex max-h-[88svh] max-w-[90vw] cursor-default flex-col items-center gap-3\"\n                  exit={{ opacity: shouldReduceMotion ? 0 : 1 }}\n                  initial={{ opacity: shouldReduceMotion ? 0 : 1 }}\n                  onClick={(event) => event.stopPropagation()}\n                  transition={{ duration: 0.12 }}\n                >\n                  {/* biome-ignore lint/performance/noImgElement: a registry component must remain framework-agnostic */}\n                  <motion.img\n                    alt={alt}\n                    className={cn(\n                      \"block max-h-[82svh] max-w-[90vw] rounded-2xl object-contain shadow-2xl ring-1 ring-white/12 will-change-transform\",\n                      previewClassName,\n                    )}\n                    layoutId={layoutId}\n                    src={src}\n                    transition={{\n                      type: \"spring\",\n                      bounce: 0,\n                      duration: shouldReduceMotion ? 0.01 : 0.38,\n                    }}\n                  />\n                  {caption ? (\n                    <motion.figcaption\n                      animate={{ opacity: 1, y: 0 }}\n                      className=\"max-w-xl text-center text-sm leading-5 text-white/72\"\n                      initial={\n                        shouldReduceMotion\n                          ? { opacity: 1, y: 0 }\n                          : { opacity: 0, y: 4 }\n                      }\n                      transition={{\n                        delay: shouldReduceMotion ? 0 : 0.14,\n                        duration: 0.18,\n                        ease: \"easeOut\",\n                      }}\n                    >\n                      {caption}\n                    </motion.figcaption>\n                  ) : null}\n                </motion.figure>\n              </Dialog.Popup>\n            </Dialog.Portal>\n          ) : null}\n        </AnimatePresence>\n      </Dialog.Root>\n    </LayoutGroup>\n  );\n}\n",
      "target": "components/ui/lightbox/lightbox.tsx"
    }
  ],
  "dependencies": [
    "@base-ui/react",
    "motion"
  ],
  "registryDependencies": [
    "@sona-ui/sona-utils"
  ]
}