{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "collection-rail",
  "type": "registry:block",
  "title": "Product rail",
  "description": "Scroll-snap product rail with prev and next buttons, a scroll progress bar and a View all link. No carousel library.",
  "author": "StoreKit <hello@storekit.dev>",
  "categories": [
    "collection",
    "any"
  ],
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "https://storekit-eight.vercel.app/r/cn.json",
    "https://storekit-eight.vercel.app/r/product-card-minimal.json"
  ],
  "files": [
    {
      "path": "collection/collection-rail.tsx",
      "content": "\"use client\"\n\nimport { useCallback, useEffect, useRef, useState } from \"react\"\nimport { ArrowRight, ChevronLeft, ChevronRight } from \"lucide-react\"\nimport { cn } from \"@/components/storekit/lib/cn\"\nimport { ProductCardMinimal, type MinimalProduct } from \"@/components/storekit/product-card/product-card-minimal\"\n\n/* Horizontal product rail on native scroll-snap. Prev and next scroll by one\n   viewport of cards, a thin bar under the rail tracks scroll position, and a\n   View all link sits in the header. The last visible card is always cut so\n   the next one peeks in, which reads as scrollable in a static view. No\n   carousel library. */\n\nexport function CollectionRail({\n  title = \"Best sellers\",\n  eyebrow = \"Shop\",\n  viewAllHref = \"#\",\n  products = [],\n  onAdd,\n  className,\n}: {\n  title?: string\n  eyebrow?: string\n  viewAllHref?: string\n  products?: (MinimalProduct & { id: string })[]\n  onAdd?: (product: MinimalProduct) => void | Promise<void>\n  className?: string\n}) {\n  const track = useRef<HTMLUListElement>(null)\n  const [pos, setPos] = useState({ progress: 0, thumb: 1, atStart: true, atEnd: true })\n\n  const measure = useCallback(() => {\n    const el = track.current\n    if (!el) return\n    const max = el.scrollWidth - el.clientWidth\n    setPos({\n      progress: max > 0 ? el.scrollLeft / max : 0,\n      thumb: el.scrollWidth > 0 ? Math.min(1, el.clientWidth / el.scrollWidth) : 1,\n      atStart: el.scrollLeft <= 2,\n      atEnd: el.scrollLeft >= max - 2,\n    })\n  }, [])\n\n  useEffect(() => {\n    measure()\n    const el = track.current\n    if (!el) return\n    const ro = new ResizeObserver(measure)\n    ro.observe(el)\n    return () => ro.disconnect()\n  }, [measure, products.length])\n\n  const scroll = (dir: 1 | -1) => {\n    const el = track.current\n    if (!el) return\n    const reduce = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n    el.scrollTo({ left: el.scrollLeft + dir * el.clientWidth * 0.9, behavior: reduce ? \"auto\" : \"smooth\" })\n  }\n\n  const navBtn =\n    \"grid h-11 w-11 place-items-center rounded-full border border-sk-line text-sk-ink outline-none transition-[transform,opacity,background-color] duration-200 ease-sk-out hover:bg-sk-ink/[0.04] active:scale-[0.97] disabled:pointer-events-none disabled:opacity-30 focus-visible:ring-2 focus-visible:ring-sk-accent focus-visible:ring-offset-2 focus-visible:ring-offset-sk-paper\"\n\n  return (\n    <div data-sk-section=\"collection\" data-sk-variant=\"collection-rail\" className={cn(\"flex flex-col gap-6\", className)}>\n      <div className=\"flex items-end justify-between gap-4\">\n        <div>\n          <p className=\"font-sk-mono text-[11px] uppercase tracking-[0.14em] text-sk-muted\">{eyebrow}</p>\n          <h2 className=\"mt-2 text-balance font-sk-display text-2xl font-semibold leading-[1.1] tracking-[-0.02em] text-sk-ink sm:text-3xl\">\n            {title}\n          </h2>\n        </div>\n        <div className=\"flex items-center gap-2\">\n          <a\n            href={viewAllHref}\n            className=\"group mr-2 hidden min-h-11 items-center gap-1.5 rounded-sk-btn text-sm font-medium text-sk-ink outline-none focus-visible:ring-2 focus-visible:ring-sk-accent focus-visible:ring-offset-2 focus-visible:ring-offset-sk-paper sm:inline-flex\"\n          >\n            View all\n            <ArrowRight size={16} aria-hidden className=\"transition-transform duration-200 ease-sk-out group-hover:translate-x-0.5\" />\n          </a>\n          <button type=\"button\" className={navBtn} onClick={() => scroll(-1)} disabled={pos.atStart} aria-label=\"Previous products\">\n            <ChevronLeft size={18} aria-hidden />\n          </button>\n          <button type=\"button\" className={navBtn} onClick={() => scroll(1)} disabled={pos.atEnd} aria-label=\"Next products\">\n            <ChevronRight size={18} aria-hidden />\n          </button>\n        </div>\n      </div>\n\n      <ul\n        ref={track}\n        onScroll={measure}\n        tabIndex={0}\n        aria-label={title}\n        className=\"-mx-4 flex snap-x snap-mandatory scroll-px-4 gap-4 overflow-x-auto px-4 pb-1 outline-none [scrollbar-width:none] focus-visible:ring-2 focus-visible:ring-sk-accent sm:mx-0 sm:scroll-px-0 sm:px-0 [&::-webkit-scrollbar]:hidden\"\n      >\n        {products.map((p) => (\n          <li key={p.id} className=\"w-[68%] shrink-0 snap-start sm:w-[calc((100%-3rem)/3.25)] lg:w-[calc((100%-4rem)/4.25)]\">\n            <ProductCardMinimal product={p} onAdd={onAdd} />\n          </li>\n        ))}\n      </ul>\n\n      <div className=\"flex items-center gap-4\">\n        <div className=\"relative h-0.5 flex-1 overflow-hidden rounded-full bg-sk-line\" aria-hidden>\n          <div\n            className=\"absolute inset-y-0 left-0 rounded-full bg-sk-ink transition-transform duration-150 ease-sk-out\"\n            style={{ width: `${pos.thumb * 100}%`, transform: `translateX(${(pos.progress * (1 - pos.thumb) * 100) / pos.thumb}%)` }}\n          />\n        </div>\n        <a\n          href={viewAllHref}\n          className=\"inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-sk-ink outline-none focus-visible:ring-2 focus-visible:ring-sk-accent sm:hidden\"\n        >\n          View all\n          <ArrowRight size={16} aria-hidden />\n        </a>\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:block",
      "target": "components/storekit/collection/collection-rail.tsx"
    }
  ],
  "meta": {
    "section": "collection",
    "variant": "rail",
    "niche": [
      "any"
    ],
    "commerce": [
      "atc"
    ],
    "previewHeight": 560
  }
}