{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "scroll-area",
  "title": "Scroll Area",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/scroll-area/ScrollArea.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface ScrollAreaProps extends HTMLAttributes<HTMLDivElement> {\n    children?: Snippet\n    ref?: HTMLDivElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { cn } from '$lib/utils'\n  import ScrollBar from './ScrollBar.svelte'\n\n  let { class: className, children, ref = $bindable(null), ...restProps }: ScrollAreaProps = $props()\n</script>\n\n<div bind:this={ref} data-uipkge data-slot=\"scroll-area\" class={cn('relative', className)} {...restProps}>\n  <div\n    data-uipkge\n    data-slot=\"scroll-area-viewport\"\n    tabindex=\"0\"\n    class=\"focus-visible:ring-ring/50 size-full overflow-auto rounded-[inherit] transition-[color,box-shadow] outline-none [scrollbar-width:none] focus-visible:ring-[3px] focus-visible:outline-1 [&::-webkit-scrollbar]:hidden\"\n  >\n    {@render children?.()}\n  </div>\n  <ScrollBar />\n  <div data-uipkge data-slot=\"scroll-area-corner\" class=\"absolute right-0 bottom-0 size-2.5\"></div>\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/scroll-area/ScrollArea.svelte"
    },
    {
      "path": "packages/registry-svelte/components/scroll-area/ScrollBar.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface ScrollBarProps extends HTMLAttributes<HTMLDivElement> {\n    orientation?: 'vertical' | 'horizontal'\n    ref?: HTMLDivElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { cn } from '$lib/utils'\n\n  let {\n    class: className,\n    orientation = 'vertical',\n    ref = $bindable(null),\n    ...restProps\n  }: ScrollBarProps = $props()\n\n  const vertical = $derived(orientation === 'vertical')\n\n  let trackEl: HTMLDivElement | null = $state(null)\n  let thumbEl: HTMLDivElement | null = $state(null)\n  let metrics = $state({ thumbSize: 0, thumbOffset: 0, scrollable: false })\n\n  function findViewport(): HTMLElement | null {\n    const root = trackEl?.closest('[data-slot=\"scroll-area\"]')\n    return (root?.querySelector('[data-slot=\"scroll-area-viewport\"]') as HTMLElement | null) ?? null\n  }\n\n  function update(viewport: HTMLElement) {\n    const client = vertical ? viewport.clientHeight : viewport.clientWidth\n    const total = vertical ? viewport.scrollHeight : viewport.scrollWidth\n    const pos = vertical ? viewport.scrollTop : viewport.scrollLeft\n    const track = vertical ? trackEl?.clientHeight ?? 0 : trackEl?.clientWidth ?? 0\n    if (total <= client || track <= 0) {\n      metrics = { thumbSize: 0, thumbOffset: 0, scrollable: false }\n      return\n    }\n    const thumbSize = Math.max(24, (client / total) * track)\n    const maxOffset = track - thumbSize\n    const thumbOffset = maxOffset <= 0 ? 0 : (pos / (total - client)) * maxOffset\n    metrics = { thumbSize, thumbOffset, scrollable: true }\n  }\n\n  $effect(() => {\n    // Track orientation so a flip re-measures with the right axis.\n    void vertical\n    const viewport = findViewport()\n    if (!viewport || !trackEl) return\n    const onScroll = () => update(viewport)\n    onScroll()\n    viewport.addEventListener('scroll', onScroll, { passive: true })\n    const ro = new ResizeObserver(onScroll)\n    ro.observe(viewport)\n    if (trackEl) ro.observe(trackEl)\n    return () => {\n      viewport.removeEventListener('scroll', onScroll)\n      ro.disconnect()\n    }\n  })\n\n  function scrollToRatio(ratio: number) {\n    const viewport = findViewport()\n    if (!viewport) return\n    const clamped = Math.min(1, Math.max(0, ratio))\n    if (vertical) {\n      viewport.scrollTop = clamped * (viewport.scrollHeight - viewport.clientHeight)\n    } else {\n      viewport.scrollLeft = clamped * (viewport.scrollWidth - viewport.clientWidth)\n    }\n  }\n\n  function onTrackPointerDown(e: PointerEvent) {\n    // Drags starting on the thumb are handled by onThumbPointerDown.\n    if (e.target !== e.currentTarget) return\n    const track = e.currentTarget as HTMLElement\n    const rect = track.getBoundingClientRect()\n    const ratio = vertical ? (e.clientY - rect.top) / rect.height : (e.clientX - rect.left) / rect.width\n    scrollToRatio(ratio - metrics.thumbSize / (vertical ? rect.height : rect.width) / 2)\n  }\n\n  function onThumbPointerDown(e: PointerEvent) {\n    e.preventDefault()\n    const thumb = e.currentTarget as HTMLElement\n    const track = trackEl\n    const viewport = findViewport()\n    if (!track || !viewport) return\n    thumb.setPointerCapture(e.pointerId)\n    const startPos = vertical ? e.clientY : e.clientX\n    const startScroll = vertical ? viewport.scrollTop : viewport.scrollLeft\n    const trackLen = vertical ? track.clientHeight : track.clientWidth\n    const scrollable = (vertical ? viewport.scrollHeight : viewport.scrollWidth) - (vertical ? viewport.clientHeight : viewport.clientWidth)\n    const denom = Math.max(1, trackLen - metrics.thumbSize)\n\n    const onMove = (ev: PointerEvent) => {\n      const delta = (vertical ? ev.clientY : ev.clientX) - startPos\n      const next = startScroll + (delta / denom) * scrollable\n      if (vertical) viewport.scrollTop = next\n      else viewport.scrollLeft = next\n    }\n    const onUp = () => {\n      thumb.removeEventListener('pointermove', onMove)\n      thumb.removeEventListener('pointerup', onUp)\n      thumb.removeEventListener('pointercancel', onUp)\n    }\n    thumb.addEventListener('pointermove', onMove)\n    thumb.addEventListener('pointerup', onUp)\n    thumb.addEventListener('pointercancel', onUp)\n  }\n\n  function onKeyDown(e: KeyboardEvent) {\n    const viewport = findViewport()\n    if (!viewport) return\n    const step = (vertical ? viewport.clientHeight : viewport.clientWidth) * 0.9 || 40\n    if (e.key === 'ArrowDown' || (e.key === 'ArrowRight' && !vertical)) {\n      e.preventDefault()\n      if (vertical) viewport.scrollTop += 40\n      else viewport.scrollLeft += 40\n    } else if (e.key === 'ArrowUp' || (e.key === 'ArrowLeft' && !vertical)) {\n      e.preventDefault()\n      if (vertical) viewport.scrollTop -= 40\n      else viewport.scrollLeft -= 40\n    } else if (e.key === 'PageDown') {\n      e.preventDefault()\n      if (vertical) viewport.scrollTop += step\n      else viewport.scrollLeft += step\n    } else if (e.key === 'PageUp') {\n      e.preventDefault()\n      if (vertical) viewport.scrollTop -= step\n      else viewport.scrollLeft -= step\n    } else if (e.key === 'Home') {\n      e.preventDefault()\n      if (vertical) viewport.scrollTop = 0\n      else viewport.scrollLeft = 0\n    } else if (e.key === 'End') {\n      e.preventDefault()\n      if (vertical) viewport.scrollTop = viewport.scrollHeight\n      else viewport.scrollLeft = viewport.scrollWidth\n    }\n  }\n</script>\n\n{#if metrics.scrollable}\n  <div\n    bind:this={ref}\n    data-uipkge\n    data-slot=\"scroll-area-scrollbar\"\n    data-orientation={orientation}\n    role=\"scrollbar\"\n    aria-orientation={orientation}\n    tabindex=\"0\"\n    class={cn(\n      'absolute flex touch-none p-px transition-colors outline-none select-none focus-visible:ring-2 focus-visible:ring-ring/50',\n      vertical ? 'top-0 right-0 h-full w-2.5 border-l border-l-transparent' : 'bottom-0 left-0 h-2.5 w-full flex-col border-t border-t-transparent',\n      className,\n    )}\n    onpointerdown={onTrackPointerDown}\n    onkeydown={onKeyDown}\n    {...restProps}\n  >\n    <!-- Thumb positioning host: svelte-check's a11y rules flag interactive\n         handlers on a plain div, so the draggable thumb carries the button role. -->\n    <div bind:this={trackEl} class=\"relative flex-1\">\n      <div\n        bind:this={thumbEl}\n        data-uipkge\n        data-slot=\"scroll-area-thumb\"\n        role=\"button\"\n        tabindex={-1}\n        aria-hidden=\"true\"\n        class=\"bg-border absolute rounded-full\"\n        style={vertical\n          ? `height: ${metrics.thumbSize}px; top: ${metrics.thumbOffset}px; left: 0; right: 0;`\n          : `width: ${metrics.thumbSize}px; left: ${metrics.thumbOffset}px; top: 0; bottom: 0;`}\n        onpointerdown={onThumbPointerDown}\n      ></div>\n    </div>\n  </div>\n{/if}\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/scroll-area/ScrollBar.svelte"
    },
    {
      "path": "packages/registry-svelte/components/scroll-area/index.ts",
      "content": "export { default as ScrollArea, type ScrollAreaProps } from './ScrollArea.svelte'\nexport { default as ScrollBar, type ScrollBarProps } from './ScrollBar.svelte'\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/scroll-area/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Custom scrollbar that always renders the same way across OSes (no flashing native scrollbars on Windows). Use for sidebars, dropdown content, and any overflow region you want to feel consistent.",
  "categories": [
    "layout"
  ]
}