{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"snap-carousel","type":"registry:component","title":"Snap carousel","description":"Snaps cards to center, drags with the mouse, and each card acts out its motion principle.","author":"Yash Bavadiya <https://xevrion.dev>","dependencies":[],"registryDependencies":["utils"],"files":[{"path":"components/snap-carousel.tsx","type":"registry:component","content":"\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ntype Slide = {\n  title: string;\n  text: string;\n  /** A small illustration above the text. Any CSS animation inside it only\n   *  runs while its card is the centered one. */\n  visual?: React.ReactNode;\n};\n\ntype Drag = {\n  id: number;\n  startX: number;\n  startScroll: number;\n  samples: { t: number; x: number }[];\n};\n\nconst CARD_WIDTH = 280;\nconst GAP = 16;\n// How long a release coasts at its own speed before picking a card. Scaled\n// with the card step (296px now, 212px when this was tuned at 200ms) so the\n// same flick still travels the same number of cards.\nconst COAST = 280;\n\n// The scrollLeft that centers each slide, read from layout so it holds for\n// any card size or padding. The scroller is positioned, so offsetLeft is\n// measured from its own content edge.\nfunction snapTargets(el: HTMLElement) {\n  const max = el.scrollWidth - el.clientWidth;\n  return Array.from(el.children as HTMLCollectionOf<HTMLElement>, (c) =>\n    Math.min(\n      max,\n      Math.max(0, c.offsetLeft + c.offsetWidth / 2 - el.clientWidth / 2),\n    ),\n  );\n}\n\n// Cards fade and shrink as they leave the center, driven by their own\n// position in the scroller rather than JS. The timeline is 'cover', so 50%\n// is the moment a card sits dead center. Browsers without scroll-driven\n// animations skip the whole block and show every card at full size, which\n// is still perfectly readable. Reduced motion keeps the fade, drops the scale.\nconst CSS = `\n@supports (animation-timeline: view()) {\n  .snap-carousel-card {\n    animation: snap-carousel-focus linear both;\n    animation-timeline: view(inline);\n  }\n  @keyframes snap-carousel-focus {\n    0%, 100% { scale: 0.9; opacity: 0.5; }\n    50% { scale: 1; opacity: 1; }\n  }\n  @media (prefers-reduced-motion: reduce) {\n    @keyframes snap-carousel-focus {\n      0%, 100% { opacity: 0.5; }\n      50% { opacity: 1; }\n    }\n  }\n}\n/* Only the centered card plays, and only while the carousel is on screen:\n   the neighbours hold whatever frame they reached, so a card picks its loop\n   back up the moment it arrives in the middle. */\n.snap-carousel-visual * { animation-play-state: paused; }\n[data-onscreen] [data-active] .snap-carousel-visual * { animation-play-state: running; }\n@media (prefers-reduced-motion: reduce) {\n  .snap-carousel-visual * { animation: none !important; }\n}\n`;\n\nexport function SnapCarousel({\n  slides,\n  label = \"Carousel\",\n  className,\n}: {\n  slides: Slide[];\n  label?: string;\n  className?: string;\n}) {\n  const scrollerRef = useRef<HTMLDivElement>(null);\n  const [atStart, setAtStart] = useState(true);\n  const [atEnd, setAtEnd] = useState(false);\n\n  // React skips the render when neither edge flag changes, so scrolling\n  // costs a couple of reads per frame, not a re-render.\n  const sync = () => {\n    const el = scrollerRef.current;\n    if (!el) return;\n    // 1px of slack absorbs fractional scroll positions on scaled displays.\n    setAtStart(el.scrollLeft <= 1);\n    setAtEnd(el.scrollLeft >= el.scrollWidth - el.clientWidth - 1);\n  };\n\n  const drag = useRef<Drag | null>(null);\n  // Set when a drag moved far enough that the click after it is not a click.\n  const suppressClick = useRef(false);\n  // Undoes the post-drag state (snap off, pending timers, listeners).\n  const settle = useRef<(() => void) | null>(null);\n\n  useEffect(() => {\n    const el = scrollerRef.current;\n    if (!el) return;\n    const update = () => {\n      setAtStart(el.scrollLeft <= 1);\n      setAtEnd(el.scrollLeft >= el.scrollWidth - el.clientWidth - 1);\n    };\n    const observer = new ResizeObserver(update);\n    observer.observe(el);\n    // Marks the fully visible (centered) card straight on the DOM, so which\n    // card is playing never costs a render.\n    const centered = new IntersectionObserver(\n      (entries) => {\n        for (const entry of entries) {\n          const slide = entry.target as HTMLElement;\n          if (entry.intersectionRatio >= 0.9) slide.dataset.active = \"\";\n          else delete slide.dataset.active;\n        }\n      },\n      { root: el, threshold: [0, 0.9] },\n    );\n    for (const child of el.children) centered.observe(child);\n    const onscreen = new IntersectionObserver(([entry]) => {\n      if (entry.isIntersecting) el.dataset.onscreen = \"\";\n      else delete el.dataset.onscreen;\n    });\n    onscreen.observe(el);\n    return () => {\n      observer.disconnect();\n      centered.disconnect();\n      onscreen.disconnect();\n      settle.current?.();\n    };\n  }, []);\n\n  // Mouse only: touch, pen and trackpads already scroll natively, and\n  // handling them here would fight the platform's own momentum.\n  const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {\n    if (e.pointerType !== \"mouse\" || e.button !== 0) return;\n    const el = e.currentTarget;\n    settle.current?.();\n    el.setPointerCapture(e.pointerId);\n    // Snap would yank the content to the nearest card on every scrollLeft\n    // write, so it stays off until the release has picked its own card.\n    el.style.scrollSnapType = \"none\";\n    el.style.scrollBehavior = \"auto\";\n    el.dataset.dragging = \"\";\n    suppressClick.current = false;\n    drag.current = {\n      id: e.pointerId,\n      startX: e.clientX,\n      startScroll: el.scrollLeft,\n      samples: [{ t: e.timeStamp, x: e.clientX }],\n    };\n  };\n\n  const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {\n    const d = drag.current;\n    if (!d || d.id !== e.pointerId) return;\n    const dx = e.clientX - d.startX;\n    // Past 4px it is a drag, not a wobbly click.\n    if (Math.abs(dx) > 4) suppressClick.current = true;\n    e.currentTarget.scrollLeft = d.startScroll - dx;\n    d.samples.push({ t: e.timeStamp, x: e.clientX });\n    // Only the last 80ms say how fast the hand was moving at release.\n    while (d.samples.length > 2 && e.timeStamp - d.samples[0].t > 80) {\n      d.samples.shift();\n    }\n  };\n\n  const onPointerEnd = (e: React.PointerEvent<HTMLDivElement>) => {\n    const d = drag.current;\n    if (!d || d.id !== e.pointerId) return;\n    drag.current = null;\n    const el = e.currentTarget;\n    delete el.dataset.dragging;\n\n    const first = d.samples[0];\n    const last = d.samples[d.samples.length - 1];\n    const recent = e.timeStamp - last.t < 80;\n    // Pixels per ms of content travel. A hand that stopped before letting\n    // go has no momentum left to carry.\n    const velocity =\n      e.type === \"pointerup\" && recent && last.t > first.t\n        ? -(last.x - first.x) / (last.t - first.t)\n        : 0;\n\n    const targets = snapTargets(el);\n    const nearest = (pos: number) =>\n      targets.reduce(\n        (best, t, i) =>\n          Math.abs(t - pos) < Math.abs(targets[best] - pos) ? i : best,\n        0,\n      );\n    // Coasts at the release speed for a short decay that turns a long fast\n    // drag into a card or two of travel, not a runaway spin.\n    let index = nearest(el.scrollLeft + velocity * COAST);\n    const from = nearest(d.startScroll);\n    // A flick faster than 300px/s always moves at least one card, however\n    // short it was.\n    if (index === from && Math.abs(velocity) > 0.3) {\n      index = Math.min(\n        targets.length - 1,\n        Math.max(0, from + Math.sign(velocity)),\n      );\n    }\n\n    const reduce = window.matchMedia(\n      \"(prefers-reduced-motion: reduce)\",\n    ).matches;\n    el.scrollTo({\n      left: targets[index],\n      behavior: reduce ? \"instant\" : \"smooth\",\n    });\n\n    // Snap comes back only once the smooth scroll has arrived; turning it on\n    // mid-flight makes the browser jump straight to the nearest card.\n    const restore = () => {\n      settle.current = null;\n      clearTimeout(fallback);\n      el.removeEventListener(\"scrollend\", restore);\n      el.style.scrollSnapType = \"\";\n      el.style.scrollBehavior = \"\";\n    };\n    // For browsers without scrollend; a smooth scroll is done well within it.\n    const fallback = setTimeout(restore, 700);\n    el.addEventListener(\"scrollend\", restore);\n    settle.current = restore;\n  };\n\n  const step = (direction: 1 | -1) => {\n    const el = scrollerRef.current;\n    if (!el) return;\n    const reduce = window.matchMedia(\n      \"(prefers-reduced-motion: reduce)\",\n    ).matches;\n    // Mandatory snapping lands this on the neighbouring card's center, even\n    // when a previous smooth scroll is still in flight.\n    el.scrollBy({\n      left: direction * (CARD_WIDTH + GAP),\n      behavior: reduce ? \"instant\" : \"smooth\",\n    });\n  };\n\n  return (\n    <div\n      className={cn(\n        \"flex w-[min(520px,100%)] flex-col items-center gap-3\",\n        className,\n      )}\n    >\n      <style href=\"snap-carousel\" precedence=\"default\">\n        {CSS}\n      </style>\n      {/* The focus ring lives on the wrapper so the edge mask below fades\n          only the cards, not the outline. */}\n      <div className=\"w-full rounded-3xl has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-solid has-[:focus-visible]:outline-offset-2 has-[:focus-visible]:outline-foreground\">\n        {/* Native scroll-snap, so touch, trackpad momentum and arrow keys all\n            behave like the platform. Inline padding of (container - 280px)\n            / 2, 120px at full width, lets the first and last cards reach the\n            center at any width; 16px of block padding keeps their shadows\n            from being clipped. The 32px mask hints there is more past each\n            edge. */}\n        <div\n          ref={scrollerRef}\n          onScroll={sync}\n          onPointerDown={onPointerDown}\n          onPointerMove={onPointerMove}\n          onPointerUp={onPointerEnd}\n          onPointerCancel={onPointerEnd}\n          onClickCapture={(e) => {\n            if (!suppressClick.current) return;\n            suppressClick.current = false;\n            e.preventDefault();\n            e.stopPropagation();\n          }}\n          onDragStart={(e) => e.preventDefault()}\n          tabIndex={0}\n          role=\"region\"\n          aria-roledescription=\"carousel\"\n          aria-label={label}\n          className=\"relative flex cursor-grab snap-x snap-mandatory gap-4 data-dragging:cursor-grabbing data-dragging:select-none overflow-x-auto overscroll-x-contain rounded-3xl px-[calc((100%-280px)/2)] py-4 outline-hidden [mask-image:linear-gradient(to_right,transparent,black_32px,black_calc(100%-32px),transparent)] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\"\n        >\n          {slides.map((slide, i) => (\n            // Snaps on the outer box and animates the inner one, so the\n            // scale never changes what the snap points measure.\n            <div\n              key={slide.title}\n              role=\"group\"\n              aria-roledescription=\"slide\"\n              aria-label={`${i + 1} of ${slides.length}`}\n              className=\"shrink-0 snap-center\"\n            >\n              <div className=\"snap-carousel-card flex h-[200px] w-[280px] flex-col justify-end gap-1.5 rounded-3xl bg-muted p-6 shadow-raised\">\n                {slide.visual && (\n                  <div\n                    aria-hidden\n                    className=\"snap-carousel-visual relative mb-auto h-[68px] w-full\"\n                  >\n                    {slide.visual}\n                  </div>\n                )}\n                <p className=\"text-[15px] font-medium text-foreground\">\n                  {slide.title}\n                </p>\n                <p className=\"text-sm leading-5.5 text-pretty text-muted-foreground\">\n                  {slide.text}\n                </p>\n              </div>\n            </div>\n          ))}\n        </div>\n      </div>\n      <div className=\"flex gap-3\">\n        <Arrow\n          label=\"Previous card\"\n          disabled={atStart}\n          onClick={() => step(-1)}\n          path=\"M10 3.5 5.5 8l4.5 4.5\"\n        />\n        <Arrow\n          label=\"Next card\"\n          disabled={atEnd}\n          onClick={() => step(1)}\n          path=\"M6 3.5 10.5 8 6 12.5\"\n        />\n      </div>\n    </div>\n  );\n}\n\nfunction Arrow({\n  label,\n  disabled,\n  onClick,\n  path,\n}: {\n  label: string;\n  disabled: boolean;\n  onClick: () => void;\n  path: string;\n}) {\n  return (\n    // aria-disabled rather than disabled: a truly disabled button throws\n    // focus back to the page the moment the last card arrives.\n    <button\n      type=\"button\"\n      aria-label={label}\n      aria-disabled={disabled}\n      onClick={() => {\n        if (!disabled) onClick();\n      }}\n      className={cn(\n        \"flex size-10 touch-manipulation items-center justify-center rounded-full bg-muted text-foreground shadow-raised outline-hidden transition-[scale,opacity] duration-150 ease-out select-none focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-2 focus-visible:outline-foreground motion-reduce:transition-[opacity]\",\n        disabled ? \"cursor-default opacity-40\" : \"active:scale-[0.96]\",\n      )}\n    >\n      <svg\n        viewBox=\"0 0 16 16\"\n        className=\"size-5\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeWidth={1.5}\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        aria-hidden\n      >\n        <path d={path} />\n      </svg>\n    </button>\n  );\n}\n\n// Each card acts out its own principle. These loops are explanatory, not UI\n// feedback, so they run well past the usual 300ms: the eye needs time to\n// compare. The track is the card's 232px content box, minus the 10px dot.\nconst DEMO_CSS = `\n.sc-dot { position: absolute; left: 0; top: 50%; margin-top: -5px; width: 10px; height: 10px; border-radius: 999px; }\n.sc-track { position: absolute; left: 5px; right: 5px; top: 50%; height: 1px; }\n\n/* Same distance, same 1.3s: only the curve differs. */\n.sc-linear { animation: sc-linear 2.6s infinite; }\n.sc-eased { animation: sc-eased 2.6s infinite; }\n@keyframes sc-linear {\n  0% { translate: 0 0; opacity: 0; animation-timing-function: linear; }\n  6% { translate: 0 0; opacity: 1; animation-timing-function: linear; }\n  56% { translate: 222px 0; opacity: 1; }\n  88% { translate: 222px 0; opacity: 1; }\n  96%, 100% { translate: 222px 0; opacity: 0; }\n}\n@keyframes sc-eased {\n  0% { translate: 0 0; opacity: 0; }\n  6% { translate: 0 0; opacity: 1; animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }\n  56% { translate: 222px 0; opacity: 1; }\n  88% { translate: 222px 0; opacity: 1; }\n  96%, 100% { translate: 222px 0; opacity: 0; }\n}\n\n/* Sampled from a real spring (stiffness 170, damping 16) heading for the\n   right tick, retargeted to the left one 260ms in. It overshoots and swings\n   back without a stop, because it kept the speed it already had. */\n.sc-spring { animation: sc-spring 2.6s linear infinite; }\n@keyframes sc-spring {\n  0% { translate: 0 0; opacity: 0; } 5% { opacity: 1; } 7.7% { translate: 0 0; } 9.2% { translate: 24.4px 0; } 10.8% { translate: 72px 0; } 12.3% { translate: 120.2px 0; } 13.8% { translate: 163.9px 0; } 15.4% { translate: 192.8px 0; } 16.9% { translate: 208.9px 0; } 18.5% { translate: 210.4px 0; } 20% { translate: 181.4px 0; } 21.5% { translate: 141.9px 0; } 23.1% { translate: 100.8px 0; } 24.6% { translate: 70.1px 0; } 26.2% { translate: 50.7px 0; } 27.7% { translate: 39.6px 0; } 29.2% { translate: 36.2px 0; } 30.8% { translate: 37px 0; } 32.3% { translate: 40.2px 0; } 33.8% { translate: 43.7px 0; } 35.4% { translate: 46.7px 0; } 36.9% { translate: 49.1px 0; } 38.5% { translate: 50.4px 0; } 40% { translate: 51px 0; } 41.5% { translate: 51.2px 0; } 44.6% { translate: 50.7px 0; } 47.7% { translate: 50.2px 0; } 50.8% { translate: 50px 0; }\n  86% { translate: 50px 0; opacity: 1; }\n  94%, 100% { translate: 50px 0; opacity: 0; }\n}\n/* The tick the spring is chasing is the solid one. */\n.sc-target-a { animation: sc-target-a 2.6s step-end infinite; }\n.sc-target-b { animation: sc-target-b 2.6s step-end infinite; }\n@keyframes sc-target-a { 0% { opacity: 1; } 17.7% { opacity: 0.2; } 94% { opacity: 1; } }\n@keyframes sc-target-b { 0% { opacity: 0.2; } 17.7% { opacity: 1; } 94% { opacity: 0.2; } }\n\n.sc-bar { animation: sc-bar 2.2s infinite; transform-origin: bottom; }\n@keyframes sc-bar {\n  0% { opacity: 0; translate: 0 10px; animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }\n  16% { opacity: 1; translate: 0 0; }\n  84% { opacity: 1; translate: 0 0; animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }\n  94%, 100% { opacity: 0; translate: 0 0; }\n}\n\n/* Two crossfades of the same square into the same circle; only the right\n   one passes through a 4px blur, and reads as one shape changing. */\n.sc-from, .sc-to { animation: 2.4s cubic-bezier(0.77, 0, 0.175, 1) infinite; }\n.sc-from { animation-name: sc-from; }\n.sc-to { animation-name: sc-to; }\n.sc-soft.sc-from { animation-name: sc-from-blur; }\n.sc-soft.sc-to { animation-name: sc-to-blur; }\n@keyframes sc-from { 0%, 30% { opacity: 1; } 50%, 80% { opacity: 0; } 100% { opacity: 1; } }\n@keyframes sc-to { 0%, 30% { opacity: 0; } 50%, 80% { opacity: 1; } 100% { opacity: 0; } }\n@keyframes sc-from-blur { 0%, 30% { opacity: 1; filter: blur(0); } 50%, 80% { opacity: 0; filter: blur(4px); } 100% { opacity: 1; filter: blur(0); } }\n@keyframes sc-to-blur { 0%, 30% { opacity: 0; filter: blur(4px); } 50%, 80% { opacity: 1; filter: blur(0); } 100% { opacity: 0; filter: blur(4px); } }\n\n/* The trigger dips as if pressed, then the panel grows out of its corner. */\n.sc-trigger { animation: sc-trigger 2.4s infinite; }\n@keyframes sc-trigger {\n  0%, 4% { scale: 1; animation-timing-function: ease-out; }\n  8% { scale: 0.96; animation-timing-function: ease-out; }\n  14%, 100% { scale: 1; }\n}\n.sc-panel { animation: sc-panel 2.4s infinite; transform-origin: 0 100%; }\n@keyframes sc-panel {\n  0%, 8% { opacity: 0; scale: 0.6; animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }\n  22% { opacity: 1; scale: 1; }\n  82% { opacity: 1; scale: 1; animation-timing-function: ease-out; }\n  90%, 100% { opacity: 0; scale: 0.96; }\n}\n`;\n\nfunction Legend({ children }: { children: React.ReactNode }) {\n  return <span className=\"font-mono text-xs text-muted-foreground\">{children}</span>;\n}\n\nfunction EaseVisual() {\n  return (\n    <div className=\"flex h-full flex-col justify-center gap-3\">\n      {[\n        { name: \"linear\", dot: \"sc-linear bg-foreground/25\" },\n        { name: \"ease-out\", dot: \"sc-eased bg-foreground\" },\n      ].map((row) => (\n        <div key={row.name} className=\"flex flex-col gap-1\">\n          <Legend>{row.name}</Legend>\n          <div className=\"relative h-2.5\">\n            <span className=\"sc-track bg-border\" />\n            <span className={cn(\"sc-dot\", row.dot)} />\n          </div>\n        </div>\n      ))}\n    </div>\n  );\n}\n\nfunction SpringVisual() {\n  return (\n    <div className=\"relative h-full\">\n      <span className=\"sc-track bg-border\" />\n      {/* Ticks sit under the dot's center at 50px and 200px of travel. */}\n      <span className=\"sc-target-b absolute top-1/2 left-[54.5px] h-4 w-px -translate-y-1/2 bg-foreground\" />\n      <span className=\"sc-target-a absolute top-1/2 left-[204.5px] h-4 w-px -translate-y-1/2 bg-foreground\" />\n      <span className=\"sc-dot sc-spring bg-foreground\" />\n    </div>\n  );\n}\n\nfunction StaggerVisual() {\n  const heights = [26, 42, 34, 52, 38, 46];\n  return (\n    <div className=\"flex h-full items-end gap-2\">\n      {heights.map((h, i) => (\n        <span\n          key={i}\n          className=\"sc-bar w-6 rounded-md bg-foreground/15 last:bg-foreground\"\n          style={{ height: h, animationDelay: `${i * 40}ms` }}\n        />\n      ))}\n    </div>\n  );\n}\n\nfunction BlurVisual() {\n  return (\n    <div className=\"flex h-full items-center justify-around\">\n      {[\n        { name: \"fade\", soft: false },\n        { name: \"fade + blur\", soft: true },\n      ].map((pair) => (\n        <div key={pair.name} className=\"flex flex-col items-center gap-2\">\n          <div className=\"grid\">\n            <span\n              className={cn(\n                \"sc-from col-start-1 row-start-1 size-10 rounded-lg bg-foreground\",\n                pair.soft && \"sc-soft\",\n              )}\n            />\n            <span\n              className={cn(\n                \"sc-to col-start-1 row-start-1 size-10 rounded-full bg-foreground\",\n                pair.soft && \"sc-soft\",\n              )}\n            />\n          </div>\n          <Legend>{pair.name}</Legend>\n        </div>\n      ))}\n    </div>\n  );\n}\n\nfunction OriginVisual() {\n  return (\n    <div className=\"relative h-full\">\n      <span className=\"sc-trigger absolute bottom-0 left-0 h-5 w-14 rounded-full bg-foreground\" />\n      {/* Its corner sits on the trigger, which is where it grows from. */}\n      <div className=\"sc-panel absolute bottom-7 left-1 flex h-11 w-32 flex-col justify-center gap-1.5 rounded-xl bg-background px-3 shadow-raised\">\n        <span className=\"h-1.5 w-16 rounded-full bg-foreground/20\" />\n        <span className=\"h-1.5 w-10 rounded-full bg-foreground/10\" />\n      </div>\n    </div>\n  );\n}\n\nfunction RestraintVisual() {\n  // The one card with nothing moving, on purpose.\n  return (\n    <div className=\"relative h-full\">\n      <span className=\"sc-track bg-border\" />\n      <span className=\"sc-dot bg-foreground\" style={{ left: \"calc(50% - 5px)\" }} />\n      <span className=\"absolute top-[calc(50%+12px)] left-1/2 -translate-x-1/2\">\n        <Legend>0ms</Legend>\n      </span>\n    </div>\n  );\n}\n\nconst SLIDES: Slide[] = [\n  { title: \"Ease out\", text: \"Starts fast, settles gently.\", visual: <EaseVisual /> },\n  { title: \"Spring\", text: \"Keeps its velocity when interrupted.\", visual: <SpringVisual /> },\n  { title: \"Stagger\", text: \"Forty milliseconds between each item.\", visual: <StaggerVisual /> },\n  { title: \"Blur\", text: \"Bridges two states into one motion.\", visual: <BlurVisual /> },\n  { title: \"Origin\", text: \"Grows from where you pointed.\", visual: <OriginVisual /> },\n  { title: \"Restraint\", text: \"Often the best animation is none.\", visual: <RestraintVisual /> },\n];\n\nexport default function SnapCarouselDemo() {\n  return (\n    <>\n      <style href=\"snap-carousel-demo\" precedence=\"default\">\n        {DEMO_CSS}\n      </style>\n      <SnapCarousel slides={SLIDES} label=\"Motion principles\" />\n    </>\n  );\n}\n"}],"cssVars":{"light":{"shadow-raised":"0 0 0 1px oklch(0 0 0 / 0.06), 0 1px 2px oklch(0 0 0 / 0.06), 0 6px 16px -6px oklch(0 0 0 / 0.12)"},"dark":{"shadow-raised":"0 0 0 1px oklch(1 0 0 / 0.08), 0 1px 2px oklch(0 0 0 / 0.4), 0 6px 16px -6px oklch(0 0 0 / 0.6)"},"theme":{"shadow-raised":"var(--shadow-raised)"}},"categories":["cards"],"docs":"From ui lab: https://lab.xevrion.dev/lab/snap-carousel"}