{
	"$schema": "https://shadcn-svelte.com/schema/registry-item.json",
	"name": "walkthrough",
	"title": "Walkthrough",
	"type": "registry:ui",
	"description": "A guided tour component with spotlight and popover.",
	"dependencies": [
		"@floating-ui/dom"
	],
	"devDependencies": [
		"@lucide/svelte@^0.544.0"
	],
	"registryDependencies": [
		"button"
	],
	"files": [
		{
			"content": "<script lang=\"ts\">\n\timport { setWalkthroughContext, type Step } from './ctx';\n\timport WalkthroughSpotlight from './walkthrough-spotlight.svelte';\n\timport WalkthroughContent from './walkthrough-content.svelte';\n\timport type { Snippet } from 'svelte';\n\n\tlet {\n\t\tsteps = [],\n\t\topen = $bindable(false),\n\t\tonComplete,\n\t\tchildren,\n\t\tpadding\n\t}: {\n\t\tsteps: Step[];\n\t\topen: boolean;\n\t\tonComplete?: () => void;\n\t\tchildren?: Snippet<[any]>;\n\t\tpadding?: number;\n\t} = $props();\n\n\tlet currentStepIndex = $state(0);\n\tlet highlightRect = $state({ top: 0, left: 0, width: 0, height: 0 });\n\n\tconst isLastStep = $derived(currentStepIndex === steps.length - 1);\n\tconst currentStep = $derived(steps[currentStepIndex]);\n\n\tfunction next() {\n\t\tif (!isLastStep) currentStepIndex++;\n\t\telse finish();\n\t}\n\n\tfunction prev() {\n\t\tif (currentStepIndex > 0) currentStepIndex--;\n\t}\n\n\tfunction finish() {\n\t\topen = false;\n\t\tsetTimeout(() => {\n\t\t\tcurrentStepIndex = 0;\n\t\t\tif (onComplete) onComplete();\n\t\t}, 300);\n\t}\n\n\tsetWalkthroughContext({\n\t\tisOpen: () => open,\n\t\tcurrentStepIndex: () => currentStepIndex,\n\t\tcurrentStep: () => currentStep,\n\t\tisLastStep: () => isLastStep,\n\t\tnext,\n\t\tprev,\n\t\tclose: () => (open = false)\n\t});\n</script>\n\n<WalkthroughSpotlight\n\t{open}\n\ttop={highlightRect.top}\n\tleft={highlightRect.left}\n\twidth={highlightRect.width}\n\theight={highlightRect.height}\n/>\n\n{#if open && currentStep}\n\t<WalkthroughContent\n\t\ttargetId={currentStep.target}\n\t\tplacement={currentStep.position}\n\t\tonUpdateRect={(rect) => (highlightRect = rect)}\n\t\tcontentSnippet={children}\n\t\t{padding}\n\t/>\n{/if}\n",
			"type": "registry:ui",
			"target": "walkthrough/walkthrough.svelte"
		},
		{
			"content": "<script lang=\"ts\">\n\timport { onDestroy } from 'svelte';\n\timport { computePosition, autoUpdate, offset, shift, arrow, flip } from '@floating-ui/dom';\n\timport { fade } from 'svelte/transition';\n\timport { getWalkthroughContext } from './ctx';\n\timport { Button } from '$lib/components/ui/button';\n\timport { X } from '@lucide/svelte';\n\timport type { Snippet } from 'svelte';\n\n\tlet {\n\t\ttargetId,\n\t\tplacement = 'bottom',\n\t\tonUpdateRect,\n\t\tcontentSnippet,\n\t\tpadding = 0\n\t}: {\n\t\t/** Omit to centre the step instead of anchoring it to an element. */\n\t\ttargetId?: string;\n\t\tplacement?: 'top' | 'bottom' | 'left' | 'right';\n\t\tonUpdateRect: (rect: { top: number; left: number; width: number; height: number }) => void;\n\t\tcontentSnippet?: Snippet<[any]>;\n\t\tpadding?: number;\n\t} = $props();\n\n\tconst ctx = getWalkthroughContext();\n\n\tlet tooltipEl: HTMLElement;\n\tlet arrowEl: HTMLElement | undefined;\n\tlet cleanup: (() => void) | undefined;\n\n\tlet actualPlacement = $state(placement);\n\t/** True when there is nothing to anchor to, so the step floats in the middle. */\n\tlet centered = $state(false);\n\n\tfunction updateSpotlight(el: HTMLElement) {\n\t\tconst rect = el.getBoundingClientRect();\n\n\t\tconst paddedWidth = rect.width + padding * 2;\n\t\tconst paddedHeight = rect.height + padding * 2;\n\t\tconst paddedTop = rect.top - padding;\n\t\tconst paddedLeft = rect.left - padding;\n\n\t\tonUpdateRect({\n\t\t\ttop: paddedTop,\n\t\t\tleft: paddedLeft,\n\t\t\twidth: paddedWidth,\n\t\t\theight: paddedHeight\n\t\t});\n\t}\n\n\t/**\n\t * Parks the step in the middle of the viewport and collapses the spotlight to\n\t * a zero sized hole, so the overlay dims everything and highlights nothing.\n\t */\n\tfunction centerTooltip() {\n\t\tif (!tooltipEl) return;\n\n\t\t// A zero sized anchor at the middle of the viewport. Going through\n\t\t// floating-ui rather than `left: 50%` matters: a `position: fixed` element\n\t\t// resolves percentages against the nearest transformed ancestor, so a\n\t\t// parent with a transform would throw the step well off centre.\n\t\tconst viewportCenter = {\n\t\t\tgetBoundingClientRect: () => {\n\t\t\t\tconst x = window.innerWidth / 2;\n\t\t\t\tconst y = window.innerHeight / 2;\n\t\t\t\treturn { width: 0, height: 0, x, y, top: y, bottom: y, left: x, right: x };\n\t\t\t}\n\t\t};\n\n\t\tconst place = () => {\n\t\t\tonUpdateRect({\n\t\t\t\ttop: window.innerHeight / 2,\n\t\t\t\tleft: window.innerWidth / 2,\n\t\t\t\twidth: 0,\n\t\t\t\theight: 0\n\t\t\t});\n\n\t\t\tcomputePosition(viewportCenter, tooltipEl, {\n\t\t\t\tplacement: 'bottom',\n\t\t\t\tstrategy: 'fixed',\n\t\t\t\t// Pulling up by half the height turns \"below the point\" into \"on it\".\n\t\t\t\tmiddleware: [offset(({ rects }) => -rects.floating.height / 2), shift({ padding: 10 })]\n\t\t\t}).then(({ x, y }) => {\n\t\t\t\tObject.assign(tooltipEl.style, {\n\t\t\t\t\tposition: 'fixed',\n\t\t\t\t\tleft: `${x}px`,\n\t\t\t\t\ttop: `${y}px`,\n\t\t\t\t\ttransform: '',\n\t\t\t\t\tdisplay: 'block'\n\t\t\t\t});\n\t\t\t});\n\t\t};\n\n\t\tplace();\n\n\t\twindow.addEventListener('resize', place);\n\t\tcleanup = () => window.removeEventListener('resize', place);\n\t}\n\n\tfunction setupFloating(targetEl: HTMLElement) {\n\t\tif (!tooltipEl) return;\n\n\t\tupdateSpotlight(targetEl);\n\n\t\tconst rect = targetEl.getBoundingClientRect();\n\t\tconst isVisible =\n\t\t\trect.top >= 0 &&\n\t\t\trect.left >= 0 &&\n\t\t\trect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n\t\t\trect.right <= (window.innerWidth || document.documentElement.clientWidth);\n\n\t\tif (!isVisible) {\n\t\t\ttargetEl.scrollIntoView({ behavior: 'smooth', block: 'center' });\n\t\t}\n\n\t\tconst middleware = [offset(12), flip(), shift({ padding: 10 })];\n\t\tif (arrowEl) middleware.push(arrow({ element: arrowEl }));\n\n\t\tcleanup = autoUpdate(targetEl, tooltipEl, () => {\n\t\t\tupdateSpotlight(targetEl);\n\n\t\t\tcomputePosition(targetEl, tooltipEl, {\n\t\t\t\tplacement,\n\t\t\t\tmiddleware,\n\t\t\t\tstrategy: 'fixed'\n\t\t\t}).then(({ x, y, placement: finalPlacement, middlewareData }) => {\n\t\t\t\tObject.assign(tooltipEl.style, {\n\t\t\t\t\tleft: `${x}px`,\n\t\t\t\t\ttop: `${y}px`,\n\t\t\t\t\tposition: 'fixed',\n\t\t\t\t\t// Cleared in case the previous step was a centred one.\n\t\t\t\t\ttransform: '',\n\t\t\t\t\tdisplay: 'block'\n\t\t\t\t});\n\n\t\t\t\tactualPlacement = finalPlacement as any;\n\n\t\t\t\tif (arrowEl && middlewareData.arrow) {\n\t\t\t\t\tconst { x: arrowX, y: arrowY } = middlewareData.arrow;\n\t\t\t\t\tconst staticSide = {\n\t\t\t\t\t\ttop: 'bottom',\n\t\t\t\t\t\tright: 'left',\n\t\t\t\t\t\tbottom: 'top',\n\t\t\t\t\t\tleft: 'right'\n\t\t\t\t\t}[finalPlacement.split('-')[0]];\n\n\t\t\t\t\tObject.assign(arrowEl.style, {\n\t\t\t\t\t\tleft: arrowX != null ? `${arrowX}px` : '',\n\t\t\t\t\t\ttop: arrowY != null ? `${arrowY}px` : '',\n\t\t\t\t\t\tright: '',\n\t\t\t\t\t\tbottom: '',\n\t\t\t\t\t\t[staticSide as string]: '-4px'\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t}\n\n\t$effect(() => {\n\t\tconst id = targetId;\n\n\t\tif (cleanup) {\n\t\t\tcleanup();\n\t\t\tcleanup = undefined;\n\t\t}\n\n\t\t// The delay lets the step's target mount before it is looked up.\n\t\tconst timer = setTimeout(() => {\n\t\t\tconst targetEl = id ? document.getElementById(id) : null;\n\n\t\t\t// No id, or an id that resolves to nothing — centre it rather than\n\t\t\t// leaving the step stranded in the corner.\n\t\t\tcentered = !targetEl;\n\n\t\t\tif (targetEl) setupFloating(targetEl);\n\t\t\telse centerTooltip();\n\t\t}, 10);\n\n\t\treturn () => {\n\t\t\tclearTimeout(timer);\n\t\t\tif (cleanup) {\n\t\t\t\tcleanup();\n\t\t\t\tcleanup = undefined;\n\t\t\t}\n\t\t};\n\t});\n\n\tonDestroy(() => {\n\t\tif (cleanup) cleanup();\n\t});\n\n\tlet arrowClasses = $derived.by(() => {\n\t\tconst side = actualPlacement.split('-')[0];\n\t\tconst base = 'absolute h-2 w-2 rotate-45 bg-popover';\n\t\tif (side === 'top') return `${base} border-b border-r`;\n\t\tif (side === 'bottom') return `${base} border-t border-l`;\n\t\tif (side === 'left') return `${base} border-t border-r`;\n\t\tif (side === 'right') return `${base} border-b border-l`;\n\t\treturn `${base} border-t border-l`;\n\t});\n</script>\n\n<div\n\tbind:this={tooltipEl}\n\trole=\"dialog\"\n\tclass=\"fixed z-[9999] top-0 left-0 w-max outline-none\"\n\ttransition:fade={{ duration: 200 }}\n>\n\t{#if contentSnippet}\n\t\t{@render contentSnippet(ctx)}\n\t{:else}\n\t\t<div class=\"relative w-[350px] rounded-lg border bg-popover text-popover-foreground shadow-xl\">\n\t\t\t{#if !centered}\n\t\t\t\t<!-- An arrow would be pointing at nothing on a centred step. -->\n\t\t\t\t<div bind:this={arrowEl} class={arrowClasses}></div>\n\t\t\t{/if}\n\n\t\t\t<div class=\"p-4\">\n\t\t\t\t<div class=\"flex items-start justify-between gap-4\">\n\t\t\t\t\t<div class=\"space-y-1\">\n\t\t\t\t\t\t<h4 class=\"font-semibold leading-none\">{ctx.currentStep()?.title}</h4>\n\t\t\t\t\t\t<p class=\"text-sm text-muted-foreground\">{ctx.currentStep()?.description}</p>\n\t\t\t\t\t</div>\n\t\t\t\t\t<Button\n\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\tclass=\"h-6 w-6 -mt-1 -mr-2 shrink-0\"\n\t\t\t\t\t\tonclick={ctx.close}\n\t\t\t\t\t>\n\t\t\t\t\t\t<X class=\"h-4 w-4\" />\n\t\t\t\t\t</Button>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"flex items-center justify-between pt-4\">\n\t\t\t\t\t<span class=\"text-xs text-muted-foreground\">\n\t\t\t\t\t\tStep {ctx.currentStepIndex() + 1}\n\t\t\t\t\t</span>\n\t\t\t\t\t<div class=\"flex gap-2\">\n\t\t\t\t\t\t{#if ctx.currentStepIndex() > 0}\n\t\t\t\t\t\t\t<Button variant=\"outline\" size=\"sm\" onclick={ctx.prev}>Back</Button>\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t<Button size=\"sm\" onclick={ctx.next}>\n\t\t\t\t\t\t\t{ctx.isLastStep() ? 'Finish' : 'Next'}\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t{/if}\n</div>\n",
			"type": "registry:ui",
			"target": "walkthrough/walkthrough-content.svelte"
		},
		{
			"content": "<script lang=\"ts\">\n\timport { fade } from 'svelte/transition';\n\n\tlet {\n\t\ttop = 0,\n\t\tleft = 0,\n\t\twidth = 0,\n\t\theight = 0,\n\t\topen = false\n\t}: {\n\t\ttop: number;\n\t\tleft: number;\n\t\twidth: number;\n\t\theight: number;\n\t\topen: boolean;\n\t} = $props();\n</script>\n\n{#if open}\n\t<div\n\t\ttransition:fade={{ duration: 300 }}\n\t\tclass=\"fixed z-[9998] pointer-events-none transition-all duration-500 ease-in-out\"\n\t\tstyle=\"\n            top: {top}px;\n            left: {left}px;\n            width: {width}px;\n            height: {height}px;\n            box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.7);\n            border-radius: 6px;\n        \"\n\t></div>\n{/if}\n",
			"type": "registry:ui",
			"target": "walkthrough/walkthrough-spotlight.svelte"
		},
		{
			"content": "import { getContext, setContext } from 'svelte';\n\nconst WALKTHROUGH_KEY = Symbol('walkthrough');\n\nexport type Step = {\n\t/**\n\t * Id of the element to anchor to. Leave it out — or point it at an element\n\t * that is not on the page — and the step is centred with nothing highlighted,\n\t * which is what you want for an intro or a closing step.\n\t */\n\ttarget?: string;\n\ttitle: string;\n\tdescription: string;\n\tposition?: 'top' | 'bottom' | 'left' | 'right';\n};\n\ntype WalkthroughContext = {\n\tisOpen: () => boolean;\n\tcurrentStepIndex: () => number;\n\tcurrentStep: () => Step | undefined;\n\tisLastStep: () => boolean;\n\tnext: () => void;\n\tprev: () => void;\n\tclose: () => void;\n};\n\nexport function setWalkthroughContext(ctx: WalkthroughContext) {\n\tsetContext(WALKTHROUGH_KEY, ctx);\n}\n\nexport function getWalkthroughContext() {\n\treturn getContext<WalkthroughContext>(WALKTHROUGH_KEY);\n}\n",
			"type": "registry:ui",
			"target": "walkthrough/ctx.ts"
		},
		{
			"content": "import Walkthrough from './walkthrough.svelte';\nexport { Walkthrough };\n",
			"type": "registry:ui",
			"target": "walkthrough/index.ts"
		}
	]
}