{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "card-scroller",
  "type": "registry:ui",
  "title": "CardScroller",
  "description": "Selectable cards in a responsive, draggable horizontal scroll-snap viewport with overflow controls and optional selected-card overlap.",
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [
    "https://components.dethink.co.uk/r/dethink-base.json",
    "https://components.dethink.co.uk/r/card.json",
    "https://components.dethink.co.uk/r/icon-button.json"
  ],
  "files": [
    {
      "path": "packages/components/src/components/card-scroller/card-scroller.tsx",
      "type": "registry:file",
      "target": "~/components/dethink/components/card-scroller/card-scroller.tsx",
      "content": "import {\n  Children,\n  cloneElement,\n  forwardRef,\n  isValidElement,\n  useCallback,\n  useId,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n  type FocusEvent,\n  type HTMLAttributes,\n  type KeyboardEvent,\n  type MouseEvent,\n  type PointerEvent,\n  type ReactElement,\n  type ReactNode,\n} from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { Card, type CardProps } from \"../card\";\nimport { IconButton } from \"../icon-button\";\n\nexport type CardScrollerMaxVisibleCards = 1 | 2 | 3 | 4;\nexport type CardScrollerCardElement = ReactElement<CardProps>;\n\nexport interface CardScrollerItemProps extends Omit<\n  HTMLAttributes<HTMLDivElement>,\n  \"children\" | \"onChange\"\n> {\n  children: CardScrollerCardElement;\n  disabled?: boolean;\n  label: string;\n  value: string;\n}\n\nexport interface CardScrollerProps extends Omit<\n  HTMLAttributes<HTMLDivElement>,\n  \"children\" | \"defaultValue\" | \"onChange\"\n> {\n  children?:\n    ReactElement<CardScrollerItemProps> | ReactElement<CardScrollerItemProps>[];\n  defaultValue?: string;\n  disabled?: boolean;\n  maxVisibleCards?: CardScrollerMaxVisibleCards;\n  name?: string;\n  nextLabel?: string;\n  onValueChange?: (value: string) => void;\n  overlap?: boolean;\n  previousLabel?: string;\n  showControls?: boolean;\n  value?: string;\n}\n\ntype InternalCardScrollerItemProps = CardScrollerItemProps & {\n  __checked?: boolean;\n  __dimmed?: boolean;\n  __inputName?: string;\n  __onSelect?: (value: string) => void;\n  __onSpotlightChange?: (value: string | null) => void;\n  __rootDisabled?: boolean;\n  __spotlighted?: boolean;\n};\n\nconst rootClasses =\n  \"group/card-scroller min-w-0 pb-[var(--dt-space-1)] text-foreground [container-type:inline-size] [--card-scroller-columns:1] [--card-scroller-gap:var(--dt-density-gap)] [--card-scroller-max-visible:3] [--card-scroller-medium-visible:2] [--card-scroller-selected-scale:1.01]\";\n\nconst viewportClasses =\n  \"flex min-w-0 snap-x snap-mandatory gap-[var(--card-scroller-gap)] overflow-x-auto overscroll-x-contain px-[var(--dt-space-4)] py-[var(--dt-space-4)] [-ms-overflow-style:none] [scroll-padding-inline:var(--dt-space-4)] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden data-[overflow=true]:cursor-grab data-[dragging=true]:cursor-grabbing data-[dragging=true]:snap-none data-[dragging=true]:select-none motion-reduce:scroll-auto @[30rem]:snap-proximity @[30rem]:[--card-scroller-columns:var(--card-scroller-medium-visible)] @[52rem]:[--card-scroller-columns:var(--card-scroller-max-visible)]\";\n\nconst itemClasses =\n  \"relative min-w-0 shrink-0 snap-start [flex-basis:calc((100%-(var(--card-scroller-columns)-1)*var(--card-scroller-gap))/var(--card-scroller-columns))] scale-[calc(var(--card-scroller-pop,1)*var(--card-scroller-press,1))] motion-safe:[transition:opacity_200ms_ease,filter_200ms_ease,scale_300ms_var(--ease-spring),translate_300ms_var(--ease-spring)] motion-reduce:transition-none data-[selected=true]:z-20 data-[dimmed=true]:opacity-60 data-[dimmed=true]:blur-[1px] data-[dimmed=true]:[--card-scroller-pop:0.99] data-[spotlighted=true]:z-30 data-[spotlighted=true]:[--card-scroller-pop:1.02] data-[spotlighted=true]:-translate-y-0.5 not-data-[disabled=true]:active:[--card-scroller-press:0.98] motion-reduce:scale-100 motion-reduce:data-[spotlighted=true]:translate-y-0 motion-reduce:data-[dimmed=true]:blur-none forced-colors:scale-100 forced-colors:data-[spotlighted=true]:translate-y-0 forced-colors:data-[dimmed=true]:opacity-100 forced-colors:data-[dimmed=true]:blur-none\";\n\nconst cardClasses =\n  \"pointer-events-none h-full cursor-pointer motion-safe:[transition:border-color_200ms_ease,box-shadow_200ms_ease,scale_300ms_var(--ease-spring)] motion-reduce:transition-none group-data-[spotlighted=true]/card-scroller-item:shadow-md data-[focus-visible=true]:ring-2 data-[focus-visible=true]:ring-inset data-[focus-visible=true]:ring-ring data-[selected=true]:border-primary data-[selected=true]:shadow-md data-[selected=true]:scale-[var(--card-scroller-selected-scale)] motion-reduce:data-[selected=true]:scale-100 forced-colors:shadow-none forced-colors:data-[focus-visible=true]:outline forced-colors:data-[focus-visible=true]:outline-2 forced-colors:data-[focus-visible=true]:outline-[Highlight] forced-colors:data-[selected=true]:outline forced-colors:data-[selected=true]:outline-2 forced-colors:data-[selected=true]:outline-[Highlight] forced-colors:data-[selected=true]:scale-100\";\n\nconst controlsClasses =\n  \"flex items-center justify-end gap-[var(--dt-space-2)] px-[var(--dt-space-3)] pt-[var(--dt-space-1)] motion-safe:transition-[opacity,translate] motion-safe:duration-200 starting:opacity-0 starting:translate-y-1\";\n\nconst navIconClasses =\n  \"motion-safe:transition-[translate] motion-safe:duration-200\";\n\nconst dragThreshold = 10;\nconst clickSuppressionDuration = 160;\n\ntype ScrollBehaviorMode = \"initial\" | \"subsequent\";\ntype HorizontalScrollAlignment = \"center\" | \"nearest\";\n\ninterface DragSession {\n  didDrag: boolean;\n  pointerId: number;\n  rtl: boolean;\n  startClientX: number;\n  startScrollLeft: number;\n}\n\nfunction ChevronLeftIcon() {\n  return (\n    <svg\n      className={cn(\n        navIconClasses,\n        \"ltr:group-hover/card-scroller-nav:-translate-x-0.5 rtl:rotate-180 rtl:group-hover/card-scroller-nav:translate-x-0.5\",\n      )}\n      aria-hidden=\"true\"\n      fill=\"none\"\n      viewBox=\"0 0 16 16\"\n      stroke=\"currentColor\"\n    >\n      <path\n        d=\"M10 3.5 5.5 8l4.5 4.5\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        strokeWidth=\"1.75\"\n      />\n    </svg>\n  );\n}\n\nfunction ChevronRightIcon() {\n  return (\n    <svg\n      className={cn(\n        navIconClasses,\n        \"ltr:group-hover/card-scroller-nav:translate-x-0.5 rtl:rotate-180 rtl:group-hover/card-scroller-nav:-translate-x-0.5\",\n      )}\n      aria-hidden=\"true\"\n      fill=\"none\"\n      viewBox=\"0 0 16 16\"\n      stroke=\"currentColor\"\n    >\n      <path\n        d=\"m6 3.5 4.5 4.5L6 12.5\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        strokeWidth=\"1.75\"\n      />\n    </svg>\n  );\n}\n\nfunction getItems(children: CardScrollerProps[\"children\"]) {\n  const seenValues = new Set<string>();\n  return Children.toArray(children).map((child) => {\n    if (\n      !isValidElement<CardScrollerItemProps>(child) ||\n      child.type !== CardScrollerItem\n    ) {\n      throw new Error(\"CardScroller expects direct CardScrollerItem children.\");\n    }\n\n    if (typeof child.props.value !== \"string\" || !child.props.value) {\n      throw new Error(\"CardScrollerItem requires a non-empty string value.\");\n    }\n    if (seenValues.has(child.props.value)) {\n      throw new Error(\"CardScrollerItem values must be unique.\");\n    }\n    seenValues.add(child.props.value);\n\n    return child;\n  });\n}\n\nfunction getCard(child: ReactNode) {\n  if (\n    Children.count(child) !== 1 ||\n    !isValidElement<CardProps>(child) ||\n    child.type !== Card\n  ) {\n    throw new Error(\"CardScrollerItem expects exactly one direct Card child.\");\n  }\n\n  return child;\n}\n\nfunction getViewportItems(viewport: HTMLElement) {\n  return Array.from(\n    viewport.querySelectorAll<HTMLElement>(\"[data-card-scroller-item]\"),\n  );\n}\n\nfunction getClosestItemIndex(\n  viewport: HTMLElement,\n  elements = getViewportItems(viewport),\n) {\n  const viewportRect = viewport.getBoundingClientRect();\n  const rtl = getComputedStyle(viewport).direction === \"rtl\";\n  const leadingEdge = rtl ? viewportRect.right : viewportRect.left;\n  let closestIndex = 0;\n  let distance = Number.POSITIVE_INFINITY;\n\n  elements.forEach((element, index) => {\n    const rect = element.getBoundingClientRect();\n    const itemEdge = rtl ? rect.right : rect.left;\n    const nextDistance = Math.abs(itemEdge - leadingEdge);\n    if (nextDistance < distance) {\n      distance = nextDistance;\n      closestIndex = index;\n    }\n  });\n\n  return closestIndex;\n}\n\nfunction getScrollBehavior(): ScrollBehavior {\n  return window.matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches\n    ? \"auto\"\n    : \"smooth\";\n}\n\nfunction scrollItemHorizontallyIntoView(\n  viewport: HTMLElement,\n  item: HTMLElement,\n  {\n    behavior,\n    inline,\n  }: {\n    behavior: ScrollBehavior;\n    inline: HorizontalScrollAlignment;\n  },\n) {\n  const viewportRect = viewport.getBoundingClientRect();\n  const itemRect = item.getBoundingClientRect();\n  const computedStyle = getComputedStyle(viewport);\n  const fallbackPadding = Number.parseFloat(computedStyle.scrollPaddingInline);\n  const startPadding =\n    Number.parseFloat(computedStyle.scrollPaddingInlineStart) ||\n    fallbackPadding ||\n    0;\n  const endPadding =\n    Number.parseFloat(computedStyle.scrollPaddingInlineEnd) ||\n    fallbackPadding ||\n    0;\n  const visibleLeft = viewportRect.left + startPadding;\n  const visibleRight = viewportRect.right - endPadding;\n  let left = 0;\n\n  if (inline === \"center\") {\n    left =\n      (itemRect.left + itemRect.right) / 2 - (visibleLeft + visibleRight) / 2;\n  } else if (itemRect.left < visibleLeft) {\n    left = itemRect.left - visibleLeft;\n  } else if (itemRect.right > visibleRight) {\n    left = itemRect.right - visibleRight;\n  }\n\n  if (Math.abs(left) < 0.5) return;\n  if (typeof viewport.scrollBy === \"function\") {\n    viewport.scrollBy({ behavior, left });\n    return;\n  }\n  viewport.scrollLeft += left;\n}\n\nexport function cardScrollerClassNames({\n  className,\n}: Pick<CardScrollerProps, \"className\"> = {}) {\n  return cn(rootClasses, className);\n}\n\nexport function cardScrollerItemClassNames({\n  className,\n}: Pick<CardScrollerItemProps, \"className\"> = {}) {\n  return cn(itemClasses, className);\n}\n\nexport const CardScrollerItem = forwardRef<\n  HTMLDivElement,\n  CardScrollerItemProps\n>((props, ref) => {\n  const {\n    __checked = false,\n    __dimmed = false,\n    __inputName,\n    __onSelect,\n    __onSpotlightChange,\n    __rootDisabled = false,\n    __spotlighted = false,\n    children,\n    className,\n    disabled = false,\n    label,\n    value,\n    ...htmlProps\n  } = props as InternalCardScrollerItemProps;\n  const card = getCard(children);\n  const isDisabled = disabled || __rootDisabled;\n  const generatedId = useId();\n  const inputId = `card-scroller-item-${generatedId}`;\n  const [hasVisibleFocus, setHasVisibleFocus] = useState(false);\n  const focusOriginRef = useRef<\"keyboard\" | \"pointer\" | null>(null);\n\n  const handleItemPointerDown = (event: PointerEvent<HTMLDivElement>) => {\n    htmlProps.onPointerDown?.(event);\n    if (event.defaultPrevented) return;\n    focusOriginRef.current = \"pointer\";\n    setHasVisibleFocus(false);\n  };\n  const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n    htmlProps.onKeyDown?.(event);\n    if (event.defaultPrevented) return;\n    focusOriginRef.current = \"keyboard\";\n    setHasVisibleFocus(true);\n  };\n  const handlePointerEnter = (event: PointerEvent<HTMLDivElement>) => {\n    htmlProps.onPointerEnter?.(event);\n    if (\n      !event.defaultPrevented &&\n      event.pointerType !== \"touch\" &&\n      window.matchMedia?.(\"(hover: hover) and (pointer: fine)\").matches\n    ) {\n      __onSpotlightChange?.(value);\n    }\n  };\n  const handlePointerLeave = (event: PointerEvent<HTMLDivElement>) => {\n    htmlProps.onPointerLeave?.(event);\n    if (!event.defaultPrevented) __onSpotlightChange?.(null);\n  };\n  const handleFocus = (event: FocusEvent<HTMLDivElement>) => {\n    htmlProps.onFocus?.(event);\n    setHasVisibleFocus(focusOriginRef.current !== \"pointer\");\n    if (!event.defaultPrevented) __onSpotlightChange?.(value);\n  };\n  const handleBlur = (event: FocusEvent<HTMLDivElement>) => {\n    htmlProps.onBlur?.(event);\n    if (!event.currentTarget.contains(event.relatedTarget)) {\n      focusOriginRef.current = null;\n      setHasVisibleFocus(false);\n      __onSpotlightChange?.(null);\n    }\n  };\n\n  return (\n    // eslint-disable-next-line jsx-a11y/no-static-element-interactions -- Keyboard events bubble from the native radio input; the wrapper adds no tab stop.\n    <div\n      {...htmlProps}\n      ref={ref}\n      data-card-scroller-item=\"\"\n      data-disabled={isDisabled ? \"true\" : undefined}\n      data-dimmed={__dimmed ? \"true\" : undefined}\n      data-selected={__checked ? \"true\" : \"false\"}\n      data-spotlighted={__spotlighted ? \"true\" : undefined}\n      className={cn(\n        \"group/card-scroller-item\",\n        cardScrollerItemClassNames({ className }),\n      )}\n      onKeyDown={handleKeyDown}\n      onPointerDown={handleItemPointerDown}\n      onPointerEnter={handlePointerEnter}\n      onPointerLeave={handlePointerLeave}\n      onFocus={handleFocus}\n      onBlur={handleBlur}\n    >\n      <input\n        id={inputId}\n        className=\"sr-only\"\n        type=\"radio\"\n        name={__inputName}\n        value={value}\n        checked={__checked}\n        disabled={isDisabled}\n        onChange={() => __onSelect?.(value)}\n      />\n      <label\n        htmlFor={inputId}\n        data-disabled={isDisabled ? \"true\" : undefined}\n        className=\"absolute inset-0 z-10 cursor-pointer data-[disabled=true]:cursor-not-allowed\"\n      >\n        <span className=\"sr-only\">{label}</span>\n      </label>\n      {cloneElement(card, {\n        className: cn(cardClasses, card.props.className),\n        \"data-focus-visible\": hasVisibleFocus ? \"true\" : undefined,\n        \"data-selected\": __checked ? \"true\" : \"false\",\n        \"aria-disabled\": isDisabled ? true : undefined,\n      } as CardProps)}\n    </div>\n  );\n});\n\nCardScrollerItem.displayName = \"CardScrollerItem\";\n\nexport const CardScroller = forwardRef<HTMLDivElement, CardScrollerProps>(\n  (\n    {\n      children,\n      className,\n      defaultValue,\n      disabled = false,\n      maxVisibleCards = 3,\n      name,\n      nextLabel = \"Next card\",\n      onValueChange,\n      overlap = false,\n      previousLabel = \"Previous card\",\n      showControls = true,\n      style,\n      value,\n      ...props\n    },\n    ref,\n  ) => {\n    const items = useMemo(() => getItems(children), [children]);\n    const generatedName = useId();\n    const inputName = name ?? `card-scroller-${generatedName}`;\n    const firstEnabledValue =\n      items.find((item) => !item.props.disabled)?.props.value ?? \"\";\n    const defaultValueIsEnabled = items.some(\n      (item) => item.props.value === defaultValue && !item.props.disabled,\n    );\n    const initialValue = defaultValueIsEnabled\n      ? (defaultValue ?? \"\")\n      : firstEnabledValue;\n    const [uncontrolledValue, setUncontrolledValue] = useState(initialValue);\n    const uncontrolledValueIsEnabled = items.some(\n      (item) => item.props.value === uncontrolledValue && !item.props.disabled,\n    );\n    const selectedValue =\n      value === undefined\n        ? uncontrolledValueIsEnabled\n          ? uncontrolledValue\n          : firstEnabledValue\n        : value;\n    const [spotlightValue, setSpotlightValue] = useState<string | null>(null);\n    const [hasOverflow, setHasOverflow] = useState(false);\n    const [isDragging, setIsDragging] = useState(false);\n    const [atStart, setAtStart] = useState(true);\n    const [atEnd, setAtEnd] = useState(true);\n    const viewportRef = useRef<HTMLDivElement>(null);\n    const didAlignInitialSelection = useRef(false);\n    const dragSessionRef = useRef<DragSession | null>(null);\n    const lastAlignedValue = useRef<string | undefined>(undefined);\n    const suppressClickUntilRef = useRef(0);\n\n    const bringValueIntoView = useCallback(\n      (nextValue: string, mode: ScrollBehaviorMode = \"subsequent\") => {\n        const viewport = viewportRef.current;\n        if (!viewport) return;\n        const selected = Array.from(\n          viewport.querySelectorAll<HTMLElement>(\"[data-card-scroller-item]\"),\n        ).find((_, index) => items[index]?.props.value === nextValue);\n        if (!selected) return;\n        scrollItemHorizontallyIntoView(viewport, selected, {\n          behavior:\n            mode === \"initial\" ||\n            window.matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches\n              ? \"auto\"\n              : \"smooth\",\n          inline: overlap ? \"center\" : \"nearest\",\n        });\n      },\n      [items, overlap],\n    );\n\n    const updateScrollState = useCallback(() => {\n      const viewport = viewportRef.current;\n      if (!viewport) return;\n      const elements = getViewportItems(viewport);\n      const first = elements[0];\n      const last = elements.at(-1);\n      const viewportRect = viewport.getBoundingClientRect();\n      const rtl = getComputedStyle(viewport).direction === \"rtl\";\n      const overflow = viewport.scrollWidth > viewport.clientWidth + 1;\n\n      setHasOverflow(overflow);\n      setAtStart(\n        !overflow ||\n          !first ||\n          (rtl\n            ? first.getBoundingClientRect().right <= viewportRect.right + 2\n            : first.getBoundingClientRect().left >= viewportRect.left - 2),\n      );\n      setAtEnd(\n        !overflow ||\n          !last ||\n          (rtl\n            ? last.getBoundingClientRect().left >= viewportRect.left - 2\n            : last.getBoundingClientRect().right <= viewportRect.right + 2),\n      );\n    }, []);\n\n    useLayoutEffect(() => {\n      const viewport = viewportRef.current;\n      if (!viewport) return;\n      updateScrollState();\n      const observer =\n        typeof ResizeObserver === \"undefined\"\n          ? null\n          : new ResizeObserver(updateScrollState);\n      observer?.observe(viewport);\n      for (const item of viewport.querySelectorAll<HTMLElement>(\n        \"[data-card-scroller-item]\",\n      ))\n        observer?.observe(item);\n      viewport.addEventListener(\"scroll\", updateScrollState, { passive: true });\n      window.addEventListener(\"resize\", updateScrollState);\n      return () => {\n        observer?.disconnect();\n        viewport.removeEventListener(\"scroll\", updateScrollState);\n        window.removeEventListener(\"resize\", updateScrollState);\n      };\n    }, [items.length, maxVisibleCards, updateScrollState]);\n\n    useLayoutEffect(() => {\n      if (!didAlignInitialSelection.current) {\n        didAlignInitialSelection.current = true;\n        lastAlignedValue.current = selectedValue;\n        if (\n          selectedValue &&\n          items.findIndex((item) => item.props.value === selectedValue) > 0\n        ) {\n          bringValueIntoView(selectedValue, \"initial\");\n        }\n        return;\n      }\n\n      if (lastAlignedValue.current === selectedValue) return;\n      lastAlignedValue.current = selectedValue;\n      if (selectedValue) bringValueIntoView(selectedValue);\n    }, [bringValueIntoView, items, selectedValue]);\n\n    const select = useCallback(\n      (nextValue: string) => {\n        if (disabled) return;\n        if (nextValue === selectedValue) return;\n        if (value === undefined) setUncontrolledValue(nextValue);\n        onValueChange?.(nextValue);\n      },\n      [disabled, onValueChange, selectedValue, value],\n    );\n\n    const alignClosestItem = useCallback(() => {\n      const viewport = viewportRef.current;\n      if (!viewport) return;\n      const elements = getViewportItems(viewport);\n      const closestIndex = getClosestItemIndex(viewport, elements);\n      elements[closestIndex]?.scrollIntoView?.({\n        behavior: getScrollBehavior(),\n        block: \"nearest\",\n        inline: \"start\",\n      });\n    }, []);\n\n    const handlePointerDown = (event: PointerEvent<HTMLDivElement>) => {\n      if (\n        disabled ||\n        !hasOverflow ||\n        event.button !== 0 ||\n        event.pointerType === \"touch\"\n      )\n        return;\n\n      dragSessionRef.current = {\n        didDrag: false,\n        pointerId: event.pointerId,\n        rtl: getComputedStyle(event.currentTarget).direction === \"rtl\",\n        startClientX: event.clientX,\n        startScrollLeft: event.currentTarget.scrollLeft,\n      };\n    };\n\n    const handlePointerMove = (event: PointerEvent<HTMLDivElement>) => {\n      const session = dragSessionRef.current;\n      if (!session || session.pointerId !== event.pointerId) return;\n      const delta = event.clientX - session.startClientX;\n\n      if (!session.didDrag && Math.abs(delta) < dragThreshold) return;\n      if (!session.didDrag) {\n        session.didDrag = true;\n        event.currentTarget.setPointerCapture?.(event.pointerId);\n        setIsDragging(true);\n        setSpotlightValue(null);\n      }\n\n      event.preventDefault();\n      const direction = session.rtl ? -1 : 1;\n      event.currentTarget.scrollLeft =\n        session.startScrollLeft - delta * direction;\n    };\n\n    const endPointerDrag = (event: PointerEvent<HTMLDivElement>) => {\n      const session = dragSessionRef.current;\n      if (!session || session.pointerId !== event.pointerId) return;\n\n      if (event.currentTarget.hasPointerCapture?.(event.pointerId)) {\n        event.currentTarget.releasePointerCapture?.(event.pointerId);\n      }\n      dragSessionRef.current = null;\n\n      if (!session.didDrag) return;\n      suppressClickUntilRef.current = Date.now() + clickSuppressionDuration;\n      setIsDragging(false);\n      alignClosestItem();\n    };\n\n    const handleClickCapture = (event: MouseEvent<HTMLDivElement>) => {\n      if (Date.now() > suppressClickUntilRef.current) return;\n      suppressClickUntilRef.current = 0;\n      event.preventDefault();\n      event.stopPropagation();\n    };\n\n    const scrollByItem = (delta: -1 | 1) => {\n      const viewport = viewportRef.current;\n      if (!viewport) return;\n      const elements = getViewportItems(viewport);\n      const currentIndex = getClosestItemIndex(viewport, elements);\n      const targetIndex = Math.min(\n        elements.length - 1,\n        Math.max(0, currentIndex + delta),\n      );\n      elements[targetIndex]?.scrollIntoView?.({\n        behavior: getScrollBehavior(),\n        block: \"nearest\",\n        inline: \"start\",\n      });\n    };\n\n    return (\n      <div\n        {...props}\n        ref={ref}\n        role=\"radiogroup\"\n        data-disabled={disabled ? \"true\" : undefined}\n        data-overlap={overlap ? \"true\" : \"false\"}\n        data-slot=\"card-scroller\"\n        className={cardScrollerClassNames({ className })}\n        style={\n          {\n            ...style,\n            \"--card-scroller-max-visible\": maxVisibleCards,\n            \"--card-scroller-medium-visible\": Math.min(maxVisibleCards, 2),\n            \"--card-scroller-gap\": overlap ? \"0px\" : \"var(--dt-density-gap)\",\n            \"--card-scroller-selected-scale\": overlap ? 1.055 : 1.01,\n          } as React.CSSProperties\n        }\n      >\n        <div\n          ref={viewportRef}\n          data-at-end={atEnd ? \"true\" : \"false\"}\n          data-at-start={atStart ? \"true\" : \"false\"}\n          data-dragging={isDragging ? \"true\" : undefined}\n          data-overflow={hasOverflow ? \"true\" : undefined}\n          data-slot=\"card-scroller-viewport\"\n          className={viewportClasses}\n          onClickCapture={handleClickCapture}\n          onDragStart={(event) => event.preventDefault()}\n          onPointerCancel={endPointerDrag}\n          onPointerDown={handlePointerDown}\n          onPointerMove={handlePointerMove}\n          onPointerUp={endPointerDrag}\n        >\n          {items.map((item) =>\n            cloneElement(item, {\n              key: item.props.value,\n              __checked: item.props.value === selectedValue,\n              __dimmed:\n                spotlightValue !== null && spotlightValue !== item.props.value,\n              __inputName: inputName,\n              __onSelect: select,\n              __onSpotlightChange: setSpotlightValue,\n              __rootDisabled: disabled,\n              __spotlighted: spotlightValue === item.props.value,\n            } as Partial<InternalCardScrollerItemProps>),\n          )}\n        </div>\n        {showControls && hasOverflow ? (\n          <div data-slot=\"card-scroller-controls\" className={controlsClasses}>\n            <IconButton\n              aria-label={previousLabel}\n              className=\"group/card-scroller-nav\"\n              disabled={disabled || atStart}\n              onClick={() => scrollByItem(-1)}\n              size=\"sm\"\n              variant=\"outline\"\n            >\n              <ChevronLeftIcon />\n            </IconButton>\n            <IconButton\n              aria-label={nextLabel}\n              className=\"group/card-scroller-nav\"\n              disabled={disabled || atEnd}\n              onClick={() => scrollByItem(1)}\n              size=\"sm\"\n              variant=\"outline\"\n            >\n              <ChevronRightIcon />\n            </IconButton>\n          </div>\n        ) : null}\n      </div>\n    );\n  },\n);\n\nCardScroller.displayName = \"CardScroller\";\n"
    },
    {
      "path": "packages/components/src/components/card-scroller/index.ts",
      "type": "registry:file",
      "target": "~/components/dethink/components/card-scroller/index.ts",
      "content": "export {\n  CardScroller,\n  CardScrollerItem,\n  cardScrollerClassNames,\n  cardScrollerItemClassNames,\n  type CardScrollerCardElement,\n  type CardScrollerItemProps,\n  type CardScrollerMaxVisibleCards,\n  type CardScrollerProps,\n} from \"./card-scroller\";\n"
    },
    {
      "path": "packages/components/src/utils/cn.ts",
      "type": "registry:file",
      "target": "~/components/dethink/utils/cn.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
    }
  ]
}
