{
	"$schema": "https://shadcn-svelte.com/schema/registry-item.json",
	"name": "arc-timeline",
	"title": "Arc Timeline",
	"type": "registry:block",
	"description": "A curved timeline that rotates through milestone steps with clickable markers, icons, and configurable spacing.",
	"files": [
		{
			"content": "<script lang=\"ts\">\n\timport type { Snippet } from \"svelte\";\n\timport { cn } from \"$UTILS$\";\n\timport type {\n\t\tArcTimelineItem,\n\t\tArcTimelineProps,\n\t\tArcTimelineRenderable,\n\t\tArcTimelineStep,\n\t} from \"./types\";\n\n\tinterface TimelineStepMeta {\n\t\tline: ArcTimelineItem;\n\t\tstep: ArcTimelineStep;\n\t\tlineIndex: number;\n\t\tstepIndex: number;\n\t\tangle: number;\n\t\tisFirstStep: boolean;\n\t\tisLastStep: boolean;\n\t}\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tdata,\n\t\tarcConfig = {},\n\t\tdefaultActiveStep = {},\n\t\t...restProps\n\t}: ArcTimelineProps = $props();\n\n\tlet circleWidth = $derived(arcConfig.circleWidth ?? 5000);\n\tlet angleBetweenMinorSteps = $derived(arcConfig.angleBetweenMinorSteps ?? 0.35);\n\tlet lineCountFillBetweenSteps = $derived(arcConfig.lineCountFillBetweenSteps ?? 10);\n\tlet boundaryPlaceholderLinesCount = $derived(arcConfig.boundaryPlaceholderLinesCount ?? 50);\n\n\tfunction getInitialCircleRotation() {\n\t\tconst defaultActiveTime = defaultActiveStep.time ?? data[0]?.time;\n\t\tconst defaultActiveStepIndex = defaultActiveStep.stepIndex ?? 0;\n\n\t\tlet count = 0;\n\n\t\tfor (const timelineItem of data) {\n\t\t\tif (timelineItem.time === defaultActiveTime) {\n\t\t\t\tcount += defaultActiveStepIndex;\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcount += timelineItem.steps.length;\n\t\t}\n\n\t\treturn (\n\t\t\t-count * angleBetweenMinorSteps * (lineCountFillBetweenSteps + 1) -\n\t\t\tangleBetweenMinorSteps * boundaryPlaceholderLinesCount\n\t\t);\n\t}\n\n\tlet circleContainerRotateDeg = $state(getInitialCircleRotation());\n\n\tlet timelineSteps = $derived.by((): TimelineStepMeta[] => {\n\t\tconst steps: TimelineStepMeta[] = [];\n\t\tlet stepsBeforeCurrentLine = 0;\n\n\t\tdata.forEach((line, lineIndex) => {\n\t\t\tline.steps.forEach((step, stepIndex) => {\n\t\t\t\tconst angle =\n\t\t\t\t\tangleBetweenMinorSteps *\n\t\t\t\t\t\t(lineCountFillBetweenSteps + 1) *\n\t\t\t\t\t\t(stepsBeforeCurrentLine + stepIndex) +\n\t\t\t\t\tangleBetweenMinorSteps * boundaryPlaceholderLinesCount;\n\n\t\t\t\tsteps.push({\n\t\t\t\t\tline,\n\t\t\t\t\tstep,\n\t\t\t\t\tlineIndex,\n\t\t\t\t\tstepIndex,\n\t\t\t\t\tangle,\n\t\t\t\t\tisFirstStep: lineIndex === 0 && stepIndex === 0,\n\t\t\t\t\tisLastStep:\n\t\t\t\t\t\tlineIndex === data.length - 1 && stepIndex === line.steps.length - 1,\n\t\t\t\t});\n\t\t\t});\n\n\t\t\tstepsBeforeCurrentLine += line.steps.length;\n\t\t});\n\n\t\treturn steps;\n\t});\n\n\tfunction getPlaceholderAngles(isFirstStep: boolean, isLastStep: boolean, angle: number) {\n\t\tconst count =\n\t\t\tisLastStep || isFirstStep ? boundaryPlaceholderLinesCount : lineCountFillBetweenSteps;\n\n\t\treturn Array.from({ length: count }, (_, index) =>\n\t\t\tisFirstStep\n\t\t\t\t? index * angleBetweenMinorSteps\n\t\t\t\t: angle + (index + 1) * angleBetweenMinorSteps\n\t\t);\n\t}\n\n\tfunction getSnippet(value: ArcTimelineRenderable): Snippet | null {\n\t\treturn typeof value === \"function\" ? value : null;\n\t}\n\n\tfunction getPrimitiveContent(value: ArcTimelineRenderable): string | number | null {\n\t\treturn typeof value === \"string\" || typeof value === \"number\" ? value : null;\n\t}\n\n\tfunction isActive(angle: number) {\n\t\treturn Math.abs(angle + circleContainerRotateDeg) < 0.01;\n\t}\n\n\tfunction setActiveAngle(angle: number) {\n\t\tcircleContainerRotateDeg = -angle;\n\t}\n\n\tfunction handleStepKeydown(event: KeyboardEvent, angle: number) {\n\t\tif (event.key === \"Enter\" || event.key === \" \") {\n\t\t\tevent.preventDefault();\n\t\t\tsetActiveAngle(angle);\n\t\t}\n\t}\n</script>\n\n<div\n\tbind:this={ref}\n\tclass={cn(\"relative h-[380px] w-full overflow-hidden\", className)}\n\t{...restProps}\n>\n\t<div\n\t\tclass=\"absolute top-28 left-1/2 aspect-square origin-center rounded-full transition-all duration-500 ease-in-out\"\n\t\tstyle:transform={`translateX(-50%) rotate(${circleContainerRotateDeg}deg)`}\n\t\tstyle:width={`${circleWidth}px`}\n\t>\n\t\t{#each timelineSteps as timelineStep (`${timelineStep.lineIndex}-${timelineStep.stepIndex}`)}\n\t\t\t{@const active = isActive(timelineStep.angle)}\n\t\t\t{@const iconSnippet = getSnippet(timelineStep.step.icon)}\n\t\t\t{@const iconText = getPrimitiveContent(timelineStep.step.icon)}\n\t\t\t{@const contentSnippet = getSnippet(timelineStep.step.content)}\n\t\t\t{@const contentText = getPrimitiveContent(timelineStep.step.content)}\n\t\t\t{@const timeSnippet = getSnippet(timelineStep.line.time)}\n\t\t\t{@const timeText = getPrimitiveContent(timelineStep.line.time)}\n\n\t\t\t{#if timelineStep.isFirstStep}\n\t\t\t\t{@const beforePlaceholderAngles = getPlaceholderAngles(\n\t\t\t\t\ttrue,\n\t\t\t\t\tfalse,\n\t\t\t\t\ttimelineStep.angle\n\t\t\t\t)}\n\n\t\t\t\t{#each beforePlaceholderAngles as fillAngle, fillIndex (`before-${fillIndex}`)}\n\t\t\t\t\t<div\n\t\t\t\t\t\tclass=\"absolute top-0 left-1/2 h-[34px] w-[1px] -translate-x-1/2\"\n\t\t\t\t\t\tstyle:transform-origin={`50% ${circleWidth / 2}px`}\n\t\t\t\t\t\tstyle:transform={`rotate(${fillAngle}deg)`}\n\t\t\t\t\t>\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclass=\"h-full w-full bg-[var(--placeholder-line-color,#a1a1a1)] dark:bg-[var(--placeholder-line-color,#737373)]\"\n\t\t\t\t\t\t\tstyle:transform-origin=\"center top\"\n\t\t\t\t\t\t\tstyle:transform={`rotate(${-fillAngle - circleContainerRotateDeg}deg)`}\n\t\t\t\t\t\t></div>\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t{/if}\n\n\t\t\t<div\n\t\t\t\tclass={cn(\n\t\t\t\t\t\"absolute top-0 left-1/2 -translate-x-1/2 cursor-pointer transition-all duration-200\",\n\t\t\t\t\tactive ? \"h-[120px] w-[2px]\" : \"h-16 w-[1.5px]\"\n\t\t\t\t)}\n\t\t\t\trole=\"button\"\n\t\t\t\ttabindex=\"0\"\n\t\t\t\tstyle:transform-origin={`50% ${circleWidth / 2}px`}\n\t\t\t\tstyle:transform={`rotate(${timelineStep.angle}deg)`}\n\t\t\t\tonclick={() => setActiveAngle(timelineStep.angle)}\n\t\t\t\tonkeydown={(event) => handleStepKeydown(event, timelineStep.angle)}\n\t\t\t>\n\t\t\t\t<div\n\t\t\t\t\tclass={cn(\n\t\t\t\t\t\t\"h-full w-full transition-colors duration-200\",\n\t\t\t\t\t\tactive\n\t\t\t\t\t\t\t? \"bg-[var(--step-line-active-color,#888888)] dark:bg-[var(--step-line-active-color,#9780ff)]\"\n\t\t\t\t\t\t\t: \"bg-[var(--step-line-inactive-color,#b1b1b1)] dark:bg-[var(--step-line-inactive-color,#737373)]\"\n\t\t\t\t\t)}\n\t\t\t\t\tstyle:transform-origin=\"center top\"\n\t\t\t\t\tstyle:transform={`rotate(${-timelineStep.angle - circleContainerRotateDeg}deg)`}\n\t\t\t\t>\n\t\t\t\t\t<div\n\t\t\t\t\t\tclass={cn(\n\t\t\t\t\t\t\t\"absolute bottom-0 left-1/2 aspect-square -translate-x-1/2\",\n\t\t\t\t\t\t\tactive\n\t\t\t\t\t\t\t\t? \"translate-y-[calc(100%_+_14px)] scale-[1.2] text-[var(--icon-active-color,#555555)] dark:text-[var(--icon-active-color,#d4d4d4)]\"\n\t\t\t\t\t\t\t\t: \"translate-y-[calc(100%_+_4px)] scale-100 text-[var(--icon-inactive-color,#a3a3a3)] dark:text-[var(--icon-inactive-color,#a3a3a3)]\"\n\t\t\t\t\t\t)}\n\t\t\t\t\t>\n\t\t\t\t\t\t{#if iconSnippet}\n\t\t\t\t\t\t\t{@render iconSnippet()}\n\t\t\t\t\t\t{:else if iconText !== null}\n\t\t\t\t\t\t\t{iconText}\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<p\n\t\t\t\t\t\tclass={cn(\n\t\t\t\t\t\t\t\"absolute bottom-0 left-1/2 line-clamp-3 flex w-[240px] -translate-x-1/2 translate-y-[calc(100%_+_42px)] items-center justify-center text-center text-sm transition-opacity duration-300 ease-in\",\n\t\t\t\t\t\t\t\"text-[var(--description-color,#555555)] dark:text-[var(--description-color,#d4d4d4)]\",\n\t\t\t\t\t\t\tactive ? \"opacity-100\" : \"opacity-0\"\n\t\t\t\t\t\t)}\n\t\t\t\t\t>\n\t\t\t\t\t\t{#if contentSnippet}\n\t\t\t\t\t\t\t{@render contentSnippet()}\n\t\t\t\t\t\t{:else if contentText !== null}\n\t\t\t\t\t\t\t{contentText}\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\n\t\t\t\t{#if timelineStep.stepIndex === 0}\n\t\t\t\t\t<div\n\t\t\t\t\t\tclass={cn(\n\t\t\t\t\t\t\t\"absolute top-0 left-1/2 z-10 -translate-x-1/2 translate-y-[calc(-100%-24px)] whitespace-nowrap\",\n\t\t\t\t\t\t\tactive\n\t\t\t\t\t\t\t\t? \"text-[var(--time-active-color,#555555)] dark:text-[var(--time-active-color,#d4d4d4)]\"\n\t\t\t\t\t\t\t\t: \"text-[var(--time-inactive-color,#a3a3a3)] dark:text-[var(--time-inactive-color,#a3a3a3)]\"\n\t\t\t\t\t\t)}\n\t\t\t\t\t>\n\t\t\t\t\t\t{#if timeSnippet}\n\t\t\t\t\t\t\t{@render timeSnippet()}\n\t\t\t\t\t\t{:else if timeText !== null}\n\t\t\t\t\t\t\t{timeText}\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t</div>\n\t\t\t\t{/if}\n\t\t\t</div>\n\n\t\t\t{@const afterPlaceholderAngles = getPlaceholderAngles(\n\t\t\t\tfalse,\n\t\t\t\ttimelineStep.isLastStep,\n\t\t\t\ttimelineStep.angle\n\t\t\t)}\n\n\t\t\t{#each afterPlaceholderAngles as fillAngle, fillIndex (`after-${fillIndex}`)}\n\t\t\t\t<div\n\t\t\t\t\tclass=\"absolute top-0 left-1/2 h-[34px] w-[1px] -translate-x-1/2\"\n\t\t\t\t\tstyle:transform-origin={`50% ${circleWidth / 2}px`}\n\t\t\t\t\tstyle:transform={`rotate(${fillAngle}deg)`}\n\t\t\t\t>\n\t\t\t\t\t<div\n\t\t\t\t\t\tclass=\"h-full w-full bg-[var(--placeholder-line-color,#a1a1a1)] dark:bg-[var(--placeholder-line-color,#737373)]\"\n\t\t\t\t\t\tstyle:transform-origin=\"center top\"\n\t\t\t\t\t\tstyle:transform={`rotate(${-fillAngle - circleContainerRotateDeg}deg)`}\n\t\t\t\t\t></div>\n\t\t\t\t</div>\n\t\t\t{/each}\n\t\t{/each}\n\t</div>\n</div>\n",
			"type": "registry:component",
			"target": "magic/arc-timeline/arc-timeline.svelte"
		},
		{
			"content": "import ArcTimeline from \"./arc-timeline.svelte\";\nexport type {\n\tArcTimelineArcConfig,\n\tArcTimelineDefaultActiveStep,\n\tArcTimelineItem,\n\tArcTimelineProps,\n\tArcTimelineRenderable,\n\tArcTimelineStep,\n} from \"./types\";\nexport { ArcTimeline };\n",
			"type": "registry:file",
			"target": "magic/arc-timeline/index.ts"
		},
		{
			"content": "import type { Snippet } from \"svelte\";\nimport type { HTMLAttributes } from \"svelte/elements\";\nimport type { WithElementRef, WithoutChildren } from \"$UTILS$\";\n\nexport type ArcTimelineRenderable = Snippet | string | number | boolean | null | undefined;\n\nexport interface ArcTimelineStep {\n\ticon: ArcTimelineRenderable;\n\tcontent: ArcTimelineRenderable;\n}\n\nexport interface ArcTimelineItem {\n\ttime: ArcTimelineRenderable;\n\tsteps: ArcTimelineStep[];\n}\n\nexport interface ArcTimelineArcConfig {\n\tcircleWidth?: number;\n\tangleBetweenMinorSteps?: number;\n\tlineCountFillBetweenSteps?: number;\n\tboundaryPlaceholderLinesCount?: number;\n}\n\nexport interface ArcTimelineDefaultActiveStep {\n\ttime?: ArcTimelineItem[\"time\"];\n\tstepIndex?: number;\n}\n\nexport type ArcTimelineProps = WithoutChildren<\n\tWithElementRef<HTMLAttributes<HTMLDivElement>, HTMLDivElement>\n> & {\n\tdata: ArcTimelineItem[];\n\tarcConfig?: ArcTimelineArcConfig;\n\tdefaultActiveStep?: ArcTimelineDefaultActiveStep;\n};\n",
			"type": "registry:file",
			"target": "magic/arc-timeline/types.ts"
		}
	]
}