File size: 2,633 Bytes
15c3607
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
<script lang="ts">
	import { ChevronLeft, ChevronRight } from '@lucide/svelte';
	import type { Snippet } from 'svelte';

	interface Props {
		class?: string;
		children?: Snippet;
		gapSize?: string;
		onScrollableChange?: (isScrollable: boolean) => void;
	}

	let { class: className = '', children, gapSize = '3', onScrollableChange }: Props = $props();

	let canScrollLeft = $state(false);
	let canScrollRight = $state(false);
	let scrollContainer: HTMLDivElement | undefined = $state();

	function scrollLeft(event?: MouseEvent) {
		event?.stopPropagation();
		event?.preventDefault();

		if (!scrollContainer) return;

		scrollContainer.scrollBy({ left: scrollContainer.clientWidth * -0.67, behavior: 'smooth' });
	}

	function scrollRight(event?: MouseEvent) {
		event?.stopPropagation();
		event?.preventDefault();

		if (!scrollContainer) return;

		scrollContainer.scrollBy({ left: scrollContainer.clientWidth * 0.67, behavior: 'smooth' });
	}

	function updateScrollButtons() {
		if (!scrollContainer) return;

		const { scrollLeft, scrollWidth, clientWidth } = scrollContainer;

		canScrollLeft = scrollLeft > 0;
		canScrollRight = scrollLeft < scrollWidth - clientWidth - 1;

		const isScrollable = scrollWidth > clientWidth;
		onScrollableChange?.(isScrollable);
	}

	export function resetScroll() {
		if (scrollContainer) {
			scrollContainer.scrollLeft = 0;
			setTimeout(() => {
				updateScrollButtons();
			}, 0);
		}
	}

	$effect(() => {
		if (!scrollContainer) return;

		const observer = new ResizeObserver(() => updateScrollButtons());
		observer.observe(scrollContainer);

		return () => observer.disconnect();
	});
</script>

<div class="relative {className}">
	<button
		class="absolute top-1/2 left-4 z-10 flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-full bg-background/25 shadow-md backdrop-blur-xs transition-opacity hover:bg-background/45 disabled:pointer-events-none disabled:opacity-0"
		onclick={scrollLeft}
		disabled={!canScrollLeft}
		aria-label="Scroll left"
	>
		<ChevronLeft class="h-4 w-4" />
	</button>

	<div
		class="scrollbar-hide flex items-start gap-{gapSize} overflow-x-auto"
		bind:this={scrollContainer}
		onscroll={updateScrollButtons}
	>
		{@render children?.()}
	</div>

	<button
		class="absolute top-1/2 right-4 z-10 flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-full bg-background/25 shadow-md backdrop-blur-xs transition-opacity hover:bg-background/45 disabled:pointer-events-none disabled:opacity-0"
		onclick={scrollRight}
		disabled={!canScrollRight}
		aria-label="Scroll right"
	>
		<ChevronRight class="h-4 w-4" />
	</button>
</div>