{
	"$schema": "https://shadcn-svelte.com/schema/registry-item.json",
	"name": "image-zoom",
	"title": "Image Zoom",
	"type": "registry:ui",
	"description": "A component to zoom into images in an overlay.",
	"dependencies": [
		"@lucide/svelte@^0.554.0"
	],
	"devDependencies": [
		"@lucide/svelte@^0.544.0"
	],
	"registryDependencies": [
		"button"
	],
	"files": [
		{
			"content": "<script lang=\"ts\">\r\n\timport { writable } from 'svelte/store';\r\n\timport { setImageZoomContext, type ZoomImageData } from './ctx';\r\n\timport { fade } from 'svelte/transition';\r\n\timport { onMount, onDestroy, type Snippet } from 'svelte';\r\n\timport { Button } from '$lib/components/ui/button';\r\n\timport { X, ChevronLeft, ChevronRight } from '@lucide/svelte';\r\n\timport { cn } from '$UTILS$';\r\n\r\n\tlet {\r\n\t\tclass: className,\r\n\t\tchildren\r\n\t}: {\r\n\t\tclass?: string;\r\n\t\tchildren?: Snippet;\r\n\t} = $props();\r\n\r\n\tconst registeredImagesStore = writable<ZoomImageData[]>([]);\r\n\tconst currentImageIndexStore = writable<number | null>(null);\r\n\tconst openStore = writable(false);\r\n\r\n\tlet registeredImages = $state<ZoomImageData[]>([]);\r\n\tlet currentImageIndex = $state<number | null>(null);\r\n\tlet isOpen = $state(false);\r\n\r\n\t$effect(() => {\r\n\t\tregisteredImages = $registeredImagesStore;\r\n\t\tcurrentImageIndex = $currentImageIndexStore;\r\n\t\tisOpen = $openStore;\r\n\t});\r\n\r\n\tconst currentImageData = $derived(\r\n\t\tcurrentImageIndex !== null ? registeredImages[currentImageIndex] : null\r\n\t);\r\n\tconst hasMultipleImages = $derived(registeredImages.length > 1);\r\n\tconst hasPrevious = $derived(currentImageIndex !== null && currentImageIndex > 0);\r\n\tconst hasNext = $derived(\r\n\t\tcurrentImageIndex !== null && currentImageIndex < registeredImages.length - 1\r\n\t);\r\n\r\n\tfunction registerImage(imageData: Omit<ZoomImageData, 'index'>) {\r\n\t\tconst index = $registeredImagesStore.length;\r\n\t\t$registeredImagesStore = [...$registeredImagesStore, { ...imageData, index }];\r\n\t\treturn index;\r\n\t}\r\n\r\n\tfunction openImage(index: number) {\r\n\t\t$currentImageIndexStore = index;\r\n\t\t$openStore = true;\r\n\t}\r\n\r\n\tfunction nextImage() {\r\n\t\tif (currentImageIndex !== null && hasNext) {\r\n\t\t\t$currentImageIndexStore = currentImageIndex + 1;\r\n\t\t}\r\n\t}\r\n\r\n\tfunction prevImage() {\r\n\t\tif (currentImageIndex !== null && hasPrevious) {\r\n\t\t\t$currentImageIndexStore = currentImageIndex - 1;\r\n\t\t}\r\n\t}\r\n\r\n\tfunction closeZoom() {\r\n\t\t$openStore = false;\r\n\t\t$currentImageIndexStore = null;\r\n\t}\r\n\r\n\tsetImageZoomContext({\r\n\t\tregisteredImages: registeredImagesStore,\r\n\t\tcurrentImageIndex: currentImageIndexStore,\r\n\t\topen: openStore,\r\n\t\tregisterImage,\r\n\t\topenImage,\r\n\t\tnextImage,\r\n\t\tprevImage\r\n\t});\r\n\r\n\tfunction handleKeydown(event: KeyboardEvent) {\r\n\t\tif (!isOpen) return;\r\n\t\tif (event.key === 'Escape') closeZoom();\r\n\t\tif (event.key === 'ArrowLeft') prevImage();\r\n\t\tif (event.key === 'ArrowRight') nextImage();\r\n\t}\r\n</script>\r\n\r\n<svelte:window onkeydown={handleKeydown} />\r\n\r\n{#if isOpen && currentImageData}\r\n\t<div\r\n\t\tclass={cn('fixed inset-0 z-[10000] flex items-center justify-center bg-black/90', className)}\r\n\t\ttransition:fade={{ duration: 150 }}\r\n\t\taria-modal=\"true\"\r\n\t\trole=\"dialog\"\r\n\t\ttabindex=\"-1\"\r\n\t>\r\n\t\t<div class=\"absolute inset-0\" onclick={closeZoom} aria-label=\"Close\" role=\"button\"></div>\r\n\r\n\t\t<div\r\n\t\t\tclass=\"relative max-w-[90vw] max-h-[90vh] flex items-center justify-center pointer-events-none\"\r\n\t\t>\r\n\t\t\t<img\r\n\t\t\t\tsrc={currentImageData.src}\r\n\t\t\t\talt={currentImageData.alt}\r\n\t\t\t\tclass=\"block max-w-full max-h-full object-contain pointer-events-auto\"\r\n\t\t\t\ttransition:fade={{ duration: 300, delay: 50 }}\r\n\t\t\t/>\r\n\t\t</div>\r\n\r\n\t\t{#if hasMultipleImages}\r\n\t\t\t<Button\r\n\t\t\t\tvariant=\"ghost\"\r\n\t\t\t\tsize=\"icon\"\r\n\t\t\t\tclass=\"absolute left-4 top-1/2 -translate-y-1/2 text-white hover:text-gray-300 hover:bg-primary cursor-pointer pointer-events-auto disabled:opacity-30 disabled:pointer-events-none\"\r\n\t\t\t\tonclick={prevImage}\r\n\t\t\t\tdisabled={!hasPrevious}\r\n\t\t\t\taria-label=\"Previous image\"\r\n\t\t\t>\r\n\t\t\t\t<ChevronLeft class=\"h-8 w-8\" />\r\n\t\t\t</Button>\r\n\r\n\t\t\t<Button\r\n\t\t\t\tvariant=\"ghost\"\r\n\t\t\t\tsize=\"icon\"\r\n\t\t\t\tclass=\"absolute right-4 top-1/2 -translate-y-1/2 text-white hover:text-gray-300 hover:bg-primary cursor-pointer pointer-events-auto disabled:opacity-30 disabled:pointer-events-none\"\r\n\t\t\t\tonclick={nextImage}\r\n\t\t\t\tdisabled={!hasNext}\r\n\t\t\t\taria-label=\"Next image\"\r\n\t\t\t>\r\n\t\t\t\t<ChevronRight class=\"h-8 w-8\" />\r\n\t\t\t</Button>\r\n\t\t{/if}\r\n\r\n\t\t<Button\r\n\t\t\tvariant=\"ghost\"\r\n\t\t\tsize=\"icon\"\r\n\t\t\tclass=\"absolute top-4 right-4 pointer-events-auto text-white hover:text-gray-300 cursor-pointer hover:bg-primary\"\r\n\t\t\tonclick={closeZoom}\r\n\t\t\taria-label=\"Close zoomed image\"\r\n\t\t>\r\n\t\t\t<X class=\"h-6 w-6\" />\r\n\t\t</Button>\r\n\t</div>\r\n{/if}\r\n\r\n{@render children()}\r\n",
			"type": "registry:ui",
			"target": "image-zoom/image-zoom.svelte"
		},
		{
			"content": "<script lang=\"ts\">\n\timport { getImageZoomContext } from './ctx';\n\timport { cn } from '$UTILS$';\n\timport type { HTMLImgAttributes } from 'svelte/elements';\n\timport { onMount } from 'svelte';\n\n\ttype Props = HTMLImgAttributes & {\n\t\tclass?: string;\n\t};\n\n\tlet { src, alt, class: className, ...rest }: Props = $props();\n\n\tconst { registerImage, openImage } = getImageZoomContext();\n\n\tlet myIndex: number;\n\n\tonMount(() => {\n\t\tif (!src) {\n\t\t\tconsole.warn(\"ImageZoom.Trigger requires a 'src' prop.\");\n\t\t\treturn;\n\t\t}\n\t\tmyIndex = registerImage({ src, alt: alt || '' });\n\t});\n\n\tfunction handleOpenZoom() {\n\t\tif (myIndex !== undefined) {\n\t\t\topenImage(myIndex);\n\t\t}\n\t}\n</script>\n\n<img\n\tsrc={src || ''}\n\talt={alt || ''}\n\tclass={cn(\n\t\t'cursor-zoom-in transition-transform duration-200 hover:scale-[1.01] hover:brightness-90',\n\t\tclassName\n\t)}\n\tonclick={handleOpenZoom}\n\t{...rest}\n/>\n",
			"type": "registry:ui",
			"target": "image-zoom/image-zoom-trigger.svelte"
		},
		{
			"content": "import { getContext, setContext } from 'svelte';\nimport type { Writable } from 'svelte/store';\n\nconst IMAGE_ZOOM_KEY = Symbol('image-zoom');\n\nexport type ZoomImageData = {\n\tsrc: string;\n\talt: string;\n\tindex: number;\n};\n\ntype ImageZoomContext = {\n\tcurrentImageIndex: Writable<number | null>;\n\topen: Writable<boolean>;\n\n\tregisterImage: (imageData: Omit<ZoomImageData, 'index'>) => number;\n\topenImage: (index: number) => void;\n\n\tnextImage: () => void;\n\tprevImage: () => void;\n\n\tregisteredImages: Writable<ZoomImageData[]>;\n};\n\nexport function setImageZoomContext(ctx: ImageZoomContext) {\n\tsetContext(IMAGE_ZOOM_KEY, ctx);\n}\n\nexport function getImageZoomContext() {\n\tconst ctx = getContext<ImageZoomContext>(IMAGE_ZOOM_KEY);\n\tif (!ctx) {\n\t\tthrow new Error('ImageZoom.Trigger must be used inside an ImageZoom.Root');\n\t}\n\treturn ctx;\n}\n",
			"type": "registry:ui",
			"target": "image-zoom/ctx.ts"
		},
		{
			"content": "import Root from \"./image-zoom.svelte\";\r\nimport Trigger from \"./image-zoom-trigger.svelte\";\r\n\r\nexport {\r\n    Root,\r\n    Trigger\r\n};",
			"type": "registry:ui",
			"target": "image-zoom/index.ts"
		}
	]
}