{
	"$schema": "https://shadcn-svelte.com/schema/registry-item.json",
	"name": "wheel-picker",
	"title": "Wheel Picker",
	"type": "registry:ui",
	"description": "An iOS-style scrollable wheel selector.",
	"files": [
		{
			"content": "<script lang=\"ts\">\n\timport { cn } from '$UTILS$';\n\timport type { Snippet } from 'svelte';\n\n\tlet {\n\t\tclass: className,\n\t\tcollapsed = false,\n\t\tchildren\n\t}: { class?: string; collapsed?: boolean; children: Snippet } = $props();\n</script>\n\n<div\n\tclass={cn(\n\t\t'relative flex w-full touch-none overflow-hidden bg-background select-none',\n\t\tclassName,\n\t\tcollapsed ? 'h-10 transition-all hover:h-[200px] active:h-[200px]' : 'h-[200px]'\n\t)}\n>\n\t<div\n\t\tclass=\"pointer-events-none absolute left-2 right-2 top-1/2 -translate-y-1/2 h-[32px] rounded-md bg-muted z-1\"\n\t/>\n\n\t<div\n\t\tclass=\"pointer-events-none absolute inset-x-0 top-0 h-16 bg-gradient-to-b from-background to-transparent z-2\"\n\t/>\n\t<div\n\t\tclass=\"pointer-events-none absolute inset-x-0 bottom-0 h-16 bg-gradient-to-t from-background to-transparent z-2\"\n\t/>\n\n\t<div class=\"flex h-full w-full justify-center px-4 z-2 perspective-container\">\n\t\t{@render children()}\n\t</div>\n</div>\n\n<style>\n\t.perspective-container {\n\t\tperspective: 1000px;\n\t}\n</style>\n",
			"type": "registry:ui",
			"target": "wheel-picker/wheel-picker.svelte"
		},
		{
			"content": "<script lang=\"ts\">\n\timport { cn } from '$UTILS$';\n\timport { setWheelContext } from './ctx.svelte';\n\timport type { Snippet } from 'svelte';\n\timport { onMount, tick } from 'svelte';\n\n\tlet {\n\t\tvalue = $bindable(),\n\t\tclass: className,\n\t\tchildren,\n\t\tonValueChange,\n\t\tloop = false\n\t}: {\n\t\tvalue?: string;\n\t\tclass?: string;\n\t\tchildren: Snippet;\n\t\tonValueChange?: (val: string) => void;\n\t\tloop?: boolean;\n\t} = $props();\n\n\tconst ITEM_HEIGHT = 32;\n\n\tlet containerRef: HTMLElement | undefined = $state();\n\tlet contentRef: HTMLElement | undefined = $state();\n\n\tlet scrollPos = $state(0);\n\tlet isDragging = $state(false);\n\tlet itemCount = $state(0);\n\n\tlet lastY = 0;\n\tlet velocity = 0;\n\tlet lastFrameTime = 0;\n\tlet animFrame: number;\n\tlet snapTimeout: ReturnType<typeof setTimeout>;\n\tlet wheelSnapTimeout: ReturnType<typeof setTimeout>;\n\n\tconst ctxState = setWheelContext({\n\t\tselectedValue: () => value,\n\t\tonSelect: (val) => scrollToItem(val),\n\t\tregister: () => itemCount++,\n\t\tunregister: () => itemCount--\n\t});\n\n\t$effect(() => {\n\t\tctxState.loop = loop;\n\t});\n\t$effect(() => {\n\t\tctxState.totalCount = itemCount;\n\t});\n\n\tfunction getMinScroll() {\n\t\tif (loop) return -Infinity;\n\t\tif (!contentRef) return 0;\n\t\treturn -(contentRef.offsetHeight - ITEM_HEIGHT);\n\t}\n\n\tfunction getMaxScroll() {\n\t\tif (loop) return Infinity;\n\t\treturn 0;\n\t}\n\n\tfunction updateContext() {\n\t\tctxState.translateY = scrollPos;\n\t\tctxState.isDragging = isDragging;\n\t}\n\n\tfunction setScroll(y: number) {\n\t\tscrollPos = y;\n\t\tupdateContext();\n\t}\n\n\tfunction getCurrentIndex() {\n\t\tlet rawIdx = Math.round(-scrollPos / ITEM_HEIGHT);\n\t\tif (loop && itemCount > 0) {\n\t\t\tconst m = itemCount;\n\t\t\treturn ((rawIdx % m) + m) % m;\n\t\t}\n\t\treturn rawIdx;\n\t}\n\n\tfunction scrollToItem(val: string) {\n\t\tif (!contentRef) return;\n\t\tconst node = contentRef.querySelector(`[data-value=\"${CSS.escape(val)}\"]`) as HTMLElement;\n\t\tif (node) {\n\t\t\tif (loop) {\n\t\t\t\tconst targetIndex = Array.from(contentRef.children).indexOf(node);\n\t\t\t\tconst currentIndex = getCurrentIndex();\n\t\t\t\tlet diff = targetIndex - currentIndex;\n\t\t\t\tif (diff > itemCount / 2) diff -= itemCount;\n\t\t\t\tif (diff < -itemCount / 2) diff += itemCount;\n\n\t\t\t\tconst targetScroll = scrollPos - diff * ITEM_HEIGHT;\n\t\t\t\tsnapTo(targetScroll);\n\t\t\t} else {\n\t\t\t\tsnapTo(-node.offsetTop);\n\t\t\t}\n\t\t}\n\t}\n\n\tfunction handleStart(y: number) {\n\t\tisDragging = true;\n\t\tlastY = y;\n\t\tlastFrameTime = Date.now();\n\t\tvelocity = 0;\n\t\tcancelAnimationFrame(animFrame);\n\t\tclearTimeout(snapTimeout);\n\t\tclearTimeout(wheelSnapTimeout);\n\t\tupdateContext();\n\t\tdocument.body.style.cursor = 'grabbing';\n\t\tdocument.body.style.userSelect = 'none';\n\t}\n\n\tfunction handleMove(y: number) {\n\t\tif (!isDragging) return;\n\t\tconst now = Date.now();\n\t\tconst dt = now - lastFrameTime;\n\t\tlastFrameTime = now;\n\t\tconst delta = y - lastY;\n\t\tlastY = y;\n\t\tif (dt > 0) {\n\t\t\tconst v = delta / dt;\n\t\t\tvelocity = 0.7 * v + 0.3 * velocity;\n\t\t}\n\t\tlet nextPos = scrollPos + delta;\n\t\tconst min = getMinScroll();\n\t\tconst max = getMaxScroll();\n\n\t\tif (!loop) {\n\t\t\tif (nextPos > max) nextPos = max + (nextPos - max) * 0.3;\n\t\t\tif (nextPos < min) nextPos = min + (nextPos - min) * 0.3;\n\t\t}\n\n\t\tsetScroll(nextPos);\n\t}\n\n\tfunction handleEnd() {\n\t\tif (!isDragging) return;\n\t\tisDragging = false;\n\t\tupdateContext();\n\t\tdocument.body.style.cursor = '';\n\t\tdocument.body.style.userSelect = '';\n\n\t\tconst loopFrame = () => {\n\t\t\tif (isDragging) return;\n\t\t\tconst friction = 0.94;\n\t\t\tvelocity *= friction;\n\t\t\tlet nextPos = scrollPos + velocity * 16;\n\t\t\tconst min = getMinScroll();\n\t\t\tconst max = getMaxScroll();\n\n\t\t\tif (!loop) {\n\t\t\t\tif (nextPos > max || nextPos < min) {\n\t\t\t\t\tsnapToNearest();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (Math.abs(velocity) < 0.05) {\n\t\t\t\tsnapToNearest();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsetScroll(nextPos);\n\t\t\tanimFrame = requestAnimationFrame(loopFrame);\n\t\t};\n\t\tloopFrame();\n\t}\n\n\tfunction snapToNearest() {\n\t\tconst rawIdx = Math.round(scrollPos / ITEM_HEIGHT);\n\t\tsnapTo(rawIdx * ITEM_HEIGHT);\n\t}\n\n\tfunction snapTo(targetY: number) {\n\t\tif (!loop) {\n\t\t\tconst min = getMinScroll();\n\t\t\tconst max = getMaxScroll();\n\t\t\ttargetY = Math.max(min, Math.min(max, targetY));\n\t\t}\n\n\t\tconst startY = scrollPos;\n\t\tconst diff = targetY - startY;\n\n\t\tif (Math.abs(diff) < 1) {\n\t\t\tsetScroll(targetY);\n\t\t\tcommitValue();\n\t\t\treturn;\n\t\t}\n\n\t\tconst startTime = Date.now();\n\t\tconst duration = 400;\n\n\t\tconst animate = () => {\n\t\t\tif (isDragging) return;\n\t\t\tconst now = Date.now();\n\t\t\tconst elapsed = now - startTime;\n\t\t\tconst t = Math.min(1, elapsed / duration);\n\t\t\tconst ease = 1 - Math.pow(1 - t, 5);\n\n\t\t\tsetScroll(startY + diff * ease);\n\n\t\t\tif (t < 1) {\n\t\t\t\tanimFrame = requestAnimationFrame(animate);\n\t\t\t} else {\n\t\t\t\tcommitValue();\n\t\t\t}\n\t\t};\n\t\tanimFrame = requestAnimationFrame(animate);\n\t}\n\n\tfunction commitValue() {\n\t\tif (!contentRef) return;\n\t\tconst idx = getCurrentIndex();\n\t\tconst child = contentRef.children[idx] as HTMLElement;\n\t\tif (child) {\n\t\t\tconst val = child.getAttribute('data-value');\n\t\t\tif (val && val !== value) {\n\t\t\t\tvalue = val;\n\t\t\t\tonValueChange?.(val);\n\t\t\t}\n\t\t}\n\t}\n\n\tfunction onWheel(e: WheelEvent) {\n\t\te.preventDefault();\n\t\tif (isDragging) return;\n\n\t\tconst dir = Math.sign(e.deltaY);\n\t\tconst step = ITEM_HEIGHT;\n\t\tlet nextY = scrollPos - dir * step;\n\n\t\tif (!loop) {\n\t\t\tconst min = getMinScroll();\n\t\t\tconst max = getMaxScroll();\n\t\t\tnextY = Math.max(min, Math.min(max, nextY));\n\t\t}\n\n\t\tsetScroll(nextY);\n\n\t\tclearTimeout(wheelSnapTimeout);\n\t\tcancelAnimationFrame(animFrame);\n\n\t\twheelSnapTimeout = setTimeout(() => {\n\t\t\tsnapToNearest();\n\t\t}, 200);\n\t}\n\n\tonMount(() => {\n\t\ttick().then(() => {\n\t\t\tif (value) scrollToItem(value);\n\t\t});\n\t\tconst onMove = (e: MouseEvent) => handleMove(e.clientY);\n\t\tconst onUp = () => handleEnd();\n\t\twindow.addEventListener('mousemove', onMove);\n\t\twindow.addEventListener('mouseup', onUp);\n\t\treturn () => {\n\t\t\twindow.removeEventListener('mousemove', onMove);\n\t\t\twindow.removeEventListener('mouseup', onUp);\n\t\t\tcancelAnimationFrame(animFrame);\n\t\t\tclearTimeout(wheelSnapTimeout);\n\t\t\tclearTimeout(snapTimeout);\n\t\t};\n\t});\n</script>\n\n<div\n\tbind:this={containerRef}\n\tclass={cn(\n\t\t'relative flex-1 h-full min-w-0 cursor-grab active:cursor-grabbing touch-none z-30',\n\t\tclassName\n\t)}\n\tonmousedown={(e) => handleStart(e.clientY)}\n\tontouchstart={(e) => {\n\t\te.preventDefault();\n\t\thandleStart(e.touches[0].clientY);\n\t}}\n\tontouchmove={(e) => {\n\t\te.preventDefault();\n\t\thandleMove(e.touches[0].clientY);\n\t}}\n\tontouchend={handleEnd}\n\tonwheel={onWheel}\n\trole=\"listbox\"\n\ttabindex=\"0\"\n>\n\t<div\n\t\tbind:this={contentRef}\n\t\tclass=\"absolute left-0 right-0 top-1/2 w-full -mt-[16px] transform-style-3d will-change-transform\"\n\t\tstyle=\"transform: translate3d(0, {scrollPos}px, 0);\"\n\t>\n\t\t{@render children()}\n\t</div>\n</div>\n",
			"type": "registry:ui",
			"target": "wheel-picker/wheel-picker-group.svelte"
		},
		{
			"content": "<script lang=\"ts\">\n\timport { cn } from '$UTILS$';\n\timport { getWheelContext } from './ctx.svelte';\n\timport type { Snippet } from 'svelte';\n\timport { onMount } from 'svelte';\n\n\tlet {\n\t\tvalue,\n\t\tclass: className,\n\t\tchildren\n\t}: {\n\t\tvalue: string;\n\t\tclass?: string;\n\t\tchildren: Snippet;\n\t} = $props();\n\n\tconst ITEM_HEIGHT = 32;\n\tconst ctx = getWheelContext();\n\n\tlet ref: HTMLElement | undefined = $state();\n\tlet offsetTop = $state(0);\n\n\tonMount(() => {\n\t\tctx.register();\n\t\tif (ref) offsetTop = ref.offsetTop;\n\t\treturn () => ctx.unregister();\n\t});\n\n\tlet parentY = $derived(ctx.state.translateY);\n\tlet loop = $derived(ctx.state.loop);\n\tlet totalCount = $derived(ctx.state.totalCount);\n\n\tlet style = $derived.by(() => {\n\t\tlet dist = parentY + offsetTop;\n\t\tlet loopOffset = 0;\n\n\t\tif (loop && totalCount > 0) {\n\t\t\tconst totalHeight = totalCount * ITEM_HEIGHT;\n\t\t\tconst halfHeight = totalHeight / 2;\n\n\t\t\tif (dist > halfHeight) {\n\t\t\t\tconst offset = -totalHeight * Math.ceil((dist - halfHeight) / totalHeight);\n\t\t\t\tdist += offset;\n\t\t\t\tloopOffset = offset;\n\t\t\t} else if (dist < -halfHeight) {\n\t\t\t\tconst offset = totalHeight * Math.ceil((-dist - halfHeight) / totalHeight);\n\t\t\t\tdist += offset;\n\t\t\t\tloopOffset = offset;\n\t\t\t}\n\t\t}\n\n\t\tconst rotateX = -dist * 0.4;\n\n\t\tif (Math.abs(rotateX) > 90) {\n\t\t\treturn {\n\t\t\t\ttransform: 'scale(0)',\n\t\t\t\topacity: 0,\n\t\t\t\tcolor: 'transparent',\n\t\t\t\tpointerEvents: 'none'\n\t\t\t};\n\t\t}\n\n\t\tconst opacity = Math.max(0.3, 1 - Math.abs(dist) / 250);\n\t\tconst isSelected = Math.abs(dist) < ITEM_HEIGHT / 2;\n\n\t\treturn {\n\t\t\ttransform: `translateY(${loopOffset}px) rotateX(${rotateX}deg) translateZ(${Math.abs(dist) * 0.3}px)`,\n\t\t\topacity,\n\t\t\tcolor: isSelected ? 'var(--foreground)' : 'var(--muted-foreground)',\n\t\t\tfontWeight: isSelected ? '600' : '400',\n\t\t\tpointerEvents: isSelected ? 'auto' : 'none'\n\t\t};\n\t});\n\n\tfunction handleClick() {\n\t\tif (!ctx.state.isDragging) ctx.onSelect(value);\n\t}\n</script>\n\n<div\n\tbind:this={ref}\n\tdata-value={value}\n\tonclick={handleClick}\n\trole=\"option\"\n\taria-selected={Math.abs(parentY + offsetTop) < 16}\n\ttabindex=\"0\"\n\tonkeydown={(e) => e.key === 'Enter' && handleClick()}\n\tclass={cn(\n\t\t'flex h-[32px] w-full items-center justify-center text-[15px] cursor-pointer select-none backface-hidden whitespace-nowrap',\n\t\tclassName\n\t)}\n\tstyle:transform={style.transform}\n\tstyle:opacity={style.opacity}\n\tstyle:color={style.color}\n\tstyle:font-weight={style.fontWeight}\n\tstyle:pointer-events={style.pointerEvents}\n>\n\t{@render children()}\n</div>\n",
			"type": "registry:ui",
			"target": "wheel-picker/wheel-picker-item.svelte"
		},
		{
			"content": "import { getContext, setContext } from 'svelte';\n\nconst WHEEL_CTX_KEY = Symbol('wheel-ctx');\n\nexport class WheelState {\n\ttranslateY = $state(0);\n\tisDragging = $state(false);\n\tloop = $state(false);\n\ttotalCount = $state(0);\n}\n\nexport function setWheelContext(props: {\n\tonSelect: (val: string) => void;\n\tselectedValue: () => string | undefined;\n\tregister: () => void;\n\tunregister: () => void;\n}) {\n\tconst state = new WheelState();\n\tsetContext(WHEEL_CTX_KEY, { ...props, state });\n\treturn state;\n}\n\nexport function getWheelContext() {\n\treturn getContext<{\n\t\tonSelect: (val: string) => void;\n\t\tselectedValue: () => string | undefined;\n\t\tregister: () => void;\n\t\tunregister: () => void;\n\t\tstate: WheelState;\n\t}>(WHEEL_CTX_KEY);\n}\n",
			"type": "registry:ui",
			"target": "wheel-picker/ctx.svelte.ts"
		},
		{
			"content": "import WheelPicker from './wheel-picker.svelte';\nimport WheelPickerGroup from './wheel-picker-group.svelte';\nimport WheelPickerItem from './wheel-picker-item.svelte';\n\nexport { WheelPicker as Root, WheelPickerGroup as Group, WheelPickerItem as Item };\n",
			"type": "registry:ui",
			"target": "wheel-picker/index.ts"
		}
	]
}