{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "carousel",
  "type": "registry:ui",
  "title": "Carousel",
  "description": "Carousel component for Tile UI.",
  "devDependencies": [
    "sass"
  ],
  "registryDependencies": [
    "@tile-ui/core",
    "button",
    "@tile-ui/styles"
  ],
  "files": [
    {
      "path": "packages/react/src/components/carousel/carousel.tsx",
      "type": "registry:ui",
      "target": "components/ui/carousel/carousel.tsx",
      "content": "import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';\nimport {\n\tcarouselStyleKeys,\n\tgetCarouselCanScrollNext,\n\tgetCarouselCanScrollPrev,\n\tgetCarouselItemScrollPosition,\n\tgetCarouselMaxScroll,\n\tgetCarouselScrollPosition,\n\tgetCarouselScrollSize,\n\tgetCarouselSelectedIndex,\n} from '../lib/core';\nimport type { CarouselBaseProps, CarouselOrientation } from '../lib/core';\nimport { Button } from '../button';\nimport styles from './carousel.module.scss';\n\ninterface CarouselContextValue {\n\torientation: CarouselOrientation;\n\tsetViewportRef: (element: HTMLDivElement | null) => void;\n\thandleScroll: () => void;\n\tscrollPrev: () => void;\n\tscrollNext: () => void;\n\tcanScrollPrev: boolean;\n\tcanScrollNext: boolean;\n\tselectedIndex: number;\n}\n\nconst CarouselContext = createContext<CarouselContextValue | null>(null);\n\nfunction useCarousel(): CarouselContextValue {\n\tconst context = useContext(CarouselContext);\n\tif (!context) {\n\t\tthrow new Error('Carousel sub-components must be used within <Carousel>.');\n\t}\n\treturn context;\n}\n\nexport interface CarouselProps extends React.HTMLAttributes<HTMLDivElement>, CarouselBaseProps {}\n\nconst Carousel = React.forwardRef<HTMLDivElement, CarouselProps>(({ className = '', orientation = 'horizontal', children, ...props }, ref) => {\n\tconst viewportRef = useRef<HTMLDivElement | null>(null);\n\tconst [canScrollPrev, setCanScrollPrev] = useState(false);\n\tconst [canScrollNext, setCanScrollNext] = useState(false);\n\tconst [selectedIndex, setSelectedIndex] = useState(0);\n\n\tconst handleScroll = useCallback(() => {\n\t\tconst viewport = viewportRef.current;\n\t\tif (!viewport) {\n\t\t\treturn;\n\t\t}\n\t\tconst position = getCarouselScrollPosition(viewport, orientation);\n\t\tconst maxScroll = getCarouselMaxScroll(viewport, orientation);\n\t\tconst itemSize = getCarouselScrollSize(viewport, orientation);\n\t\tsetCanScrollPrev(getCarouselCanScrollPrev(position));\n\t\tsetCanScrollNext(getCarouselCanScrollNext(position, maxScroll));\n\t\tsetSelectedIndex(getCarouselSelectedIndex(position, itemSize));\n\t}, [orientation]);\n\n\tconst setViewportRef = useCallback(\n\t\t(element: HTMLDivElement | null) => {\n\t\t\tviewportRef.current = element;\n\t\t\tif (element) {\n\t\t\t\thandleScroll();\n\t\t\t}\n\t\t},\n\t\t[handleScroll],\n\t);\n\n\tconst scrollPrev = useCallback(() => {\n\t\tconst viewport = viewportRef.current;\n\t\tif (!viewport) {\n\t\t\treturn;\n\t\t}\n\t\tconst items = Array.from(viewport.querySelectorAll<HTMLElement>('[data-slot=\"carousel-item\"]'));\n\t\tif (items.length === 0) {\n\t\t\treturn;\n\t\t}\n\t\tconst position = getCarouselScrollPosition(viewport, orientation);\n\t\tconst currentIndex = getCarouselSelectedIndex(position, getCarouselScrollSize(viewport, orientation));\n\t\tconst target = items[Math.max(0, currentIndex - 1)];\n\t\tconst targetPosition = getCarouselItemScrollPosition(items[0], target, orientation);\n\t\tif (orientation === 'horizontal') {\n\t\t\tviewport.scrollTo({ left: targetPosition, behavior: 'smooth' });\n\t\t} else {\n\t\t\tviewport.scrollTo({ top: targetPosition, behavior: 'smooth' });\n\t\t}\n\t}, [orientation]);\n\n\tconst scrollNext = useCallback(() => {\n\t\tconst viewport = viewportRef.current;\n\t\tif (!viewport) {\n\t\t\treturn;\n\t\t}\n\t\tconst items = Array.from(viewport.querySelectorAll<HTMLElement>('[data-slot=\"carousel-item\"]'));\n\t\tif (items.length === 0) {\n\t\t\treturn;\n\t\t}\n\t\tconst position = getCarouselScrollPosition(viewport, orientation);\n\t\tconst currentIndex = getCarouselSelectedIndex(position, getCarouselScrollSize(viewport, orientation));\n\t\tconst target = items[Math.min(items.length - 1, currentIndex + 1)];\n\t\tconst targetPosition = getCarouselItemScrollPosition(items[0], target, orientation);\n\t\tif (orientation === 'horizontal') {\n\t\t\tviewport.scrollTo({ left: targetPosition, behavior: 'smooth' });\n\t\t} else {\n\t\t\tviewport.scrollTo({ top: targetPosition, behavior: 'smooth' });\n\t\t}\n\t}, [orientation]);\n\n\tuseEffect(() => {\n\t\tconst viewport = viewportRef.current;\n\t\tif (!viewport) {\n\t\t\treturn;\n\t\t}\n\t\thandleScroll();\n\t\tconst observer = new ResizeObserver(() => handleScroll());\n\t\tobserver.observe(viewport);\n\t\treturn () => observer.disconnect();\n\t}, [handleScroll]);\n\n\tfunction handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {\n\t\tconst isHorizontal = orientation === 'horizontal';\n\t\tif (event.key === (isHorizontal ? 'ArrowLeft' : 'ArrowUp')) {\n\t\t\tevent.preventDefault();\n\t\t\tscrollPrev();\n\t\t} else if (event.key === (isHorizontal ? 'ArrowRight' : 'ArrowDown')) {\n\t\t\tevent.preventDefault();\n\t\t\tscrollNext();\n\t\t}\n\t}\n\n\treturn (\n\t\t<CarouselContext.Provider value={{ orientation, setViewportRef, handleScroll, scrollPrev, scrollNext, canScrollPrev, canScrollNext, selectedIndex }}>\n\t\t\t<div\n\t\t\t\tref={ref}\n\t\t\t\trole=\"region\"\n\t\t\t\taria-roledescription=\"carousel\"\n\t\t\t\tdata-slot=\"carousel\"\n\t\t\t\tdata-orientation={orientation}\n\t\t\t\tonKeyDownCapture={handleKeyDown}\n\t\t\t\tclassName={`${styles[carouselStyleKeys.root]} ${className}`}\n\t\t\t\t{...props}>\n\t\t\t\t{children}\n\t\t\t</div>\n\t\t</CarouselContext.Provider>\n\t);\n});\nCarousel.displayName = 'Carousel';\n\nexport interface CarouselContentProps extends React.HTMLAttributes<HTMLDivElement> {}\n\nconst CarouselContent = React.forwardRef<HTMLDivElement, CarouselContentProps>(({ className = '', children, ...props }, ref) => {\n\tconst { setViewportRef, handleScroll } = useCarousel();\n\n\tfunction setRef(element: HTMLDivElement | null) {\n\t\tsetViewportRef(element);\n\t\tif (typeof ref === 'function') {\n\t\t\tref(element);\n\t\t} else if (ref) {\n\t\t\tref.current = element;\n\t\t}\n\t}\n\n\treturn (\n\t\t<div ref={setRef} data-slot=\"carousel-content\" onScroll={handleScroll} className={styles[carouselStyleKeys.viewport]}>\n\t\t\t<div className={`${styles[carouselStyleKeys.container]} ${className}`} {...props}>\n\t\t\t\t{children}\n\t\t\t</div>\n\t\t</div>\n\t);\n});\nCarouselContent.displayName = 'CarouselContent';\n\nexport interface CarouselItemProps extends React.HTMLAttributes<HTMLDivElement> {}\n\nconst CarouselItem = React.forwardRef<HTMLDivElement, CarouselItemProps>(({ className = '', ...props }, ref) => {\n\treturn <div ref={ref} role=\"group\" aria-roledescription=\"slide\" data-slot=\"carousel-item\" className={`${styles[carouselStyleKeys.item]} ${className}`} {...props} />;\n});\nCarouselItem.displayName = 'CarouselItem';\n\nexport interface CarouselPreviousProps extends React.ComponentProps<typeof Button> {}\n\nconst CarouselPrevious = React.forwardRef<HTMLButtonElement, CarouselPreviousProps>(({ className = '', variant = 'outline', size = 'icon', ...props }, ref) => {\n\tconst { scrollPrev, canScrollPrev } = useCarousel();\n\n\treturn (\n\t\t<Button\n\t\t\tref={ref}\n\t\t\tdata-slot=\"carousel-previous\"\n\t\t\tvariant={variant}\n\t\t\tsize={size}\n\t\t\tclassName={`${styles[carouselStyleKeys.previous]} ${className}`}\n\t\t\tdisabled={!canScrollPrev}\n\t\t\tonClick={scrollPrev}\n\t\t\t{...props}>\n\t\t\t<svg\n\t\t\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\t\t\twidth=\"16\"\n\t\t\t\theight=\"16\"\n\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\tfill=\"none\"\n\t\t\t\tstroke=\"currentColor\"\n\t\t\t\tstrokeWidth=\"2\"\n\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t\taria-hidden=\"true\">\n\t\t\t\t<path d=\"M19 12H5\" />\n\t\t\t\t<path d=\"m12 19-7-7 7-7\" />\n\t\t\t</svg>\n\t\t\t<span\n\t\t\t\tstyle={{\n\t\t\t\t\tposition: 'absolute',\n\t\t\t\t\twidth: '1px',\n\t\t\t\t\theight: '1px',\n\t\t\t\t\tpadding: 0,\n\t\t\t\t\tmargin: '-1px',\n\t\t\t\t\toverflow: 'hidden',\n\t\t\t\t\tclip: 'rect(0,0,0,0)',\n\t\t\t\t\twhiteSpace: 'nowrap',\n\t\t\t\t\tborderWidth: 0,\n\t\t\t\t}}>\n\t\t\t\tPrevious slide\n\t\t\t</span>\n\t\t</Button>\n\t);\n});\nCarouselPrevious.displayName = 'CarouselPrevious';\n\nexport interface CarouselNextProps extends React.ComponentProps<typeof Button> {}\n\nconst CarouselNext = React.forwardRef<HTMLButtonElement, CarouselNextProps>(({ className = '', variant = 'outline', size = 'icon', ...props }, ref) => {\n\tconst { scrollNext, canScrollNext } = useCarousel();\n\n\treturn (\n\t\t<Button\n\t\t\tref={ref}\n\t\t\tdata-slot=\"carousel-next\"\n\t\t\tvariant={variant}\n\t\t\tsize={size}\n\t\t\tclassName={`${styles[carouselStyleKeys.next]} ${className}`}\n\t\t\tdisabled={!canScrollNext}\n\t\t\tonClick={scrollNext}\n\t\t\t{...props}>\n\t\t\t<svg\n\t\t\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\t\t\twidth=\"16\"\n\t\t\t\theight=\"16\"\n\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\tfill=\"none\"\n\t\t\t\tstroke=\"currentColor\"\n\t\t\t\tstrokeWidth=\"2\"\n\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t\taria-hidden=\"true\">\n\t\t\t\t<path d=\"M5 12h14\" />\n\t\t\t\t<path d=\"m12 5 7 7-7 7\" />\n\t\t\t</svg>\n\t\t\t<span\n\t\t\t\tstyle={{\n\t\t\t\t\tposition: 'absolute',\n\t\t\t\t\twidth: '1px',\n\t\t\t\t\theight: '1px',\n\t\t\t\t\tpadding: 0,\n\t\t\t\t\tmargin: '-1px',\n\t\t\t\t\toverflow: 'hidden',\n\t\t\t\t\tclip: 'rect(0,0,0,0)',\n\t\t\t\t\twhiteSpace: 'nowrap',\n\t\t\t\t\tborderWidth: 0,\n\t\t\t\t}}>\n\t\t\t\tNext slide\n\t\t\t</span>\n\t\t</Button>\n\t);\n});\nCarouselNext.displayName = 'CarouselNext';\n\nexport { Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext };\nexport default Carousel;\n"
    },
    {
      "path": "packages/styles/scss/components/carousel.module.scss",
      "type": "registry:file",
      "target": "components/ui/carousel/carousel.module.scss",
      "content": "@use '../../../styles/variables/colors' as *;\n\n// 轮播根容器\n.root {\n\tposition: relative;\n}\n\n// 滚动视口\n.viewport {\n\toverflow: hidden;\n\tscroll-snap-type: x mandatory;\n\n\t// 隐藏滚动条\n\t&::-webkit-scrollbar {\n\t\tdisplay: none;\n\t}\n\n\tscrollbar-width: none;\n}\n\n// 内部 flex 容器\n.container {\n\tdisplay: flex;\n\tmargin-left: -$spacing-4;\n}\n\n// 单个 item\n.item {\n\tflex: 0 0 100%;\n\tmin-width: 0;\n\tpadding-left: $spacing-4;\n\tscroll-snap-align: start;\n}\n\n// 上一张 / 下一张按钮 (仅定位，尺寸沿用 Button)\n.previous,\n.next {\n\tposition: absolute;\n\ttop: 50%;\n\ttransform: translateY(-50%);\n}\n\n.previous {\n\tleft: $spacing-4;\n}\n\n.next {\n\tright: $spacing-4;\n}\n\n// 垂直方向\n.root[data-orientation='vertical'] {\n\t.viewport {\n\t\tscroll-snap-type: y mandatory;\n\t}\n\n\t.container {\n\t\tflex-direction: column;\n\t\tmargin-left: 0;\n\t\tmargin-top: -$spacing-4;\n\t}\n\n\t.item {\n\t\tpadding-left: 0;\n\t\tpadding-top: $spacing-4;\n\t}\n\n\t.previous,\n\t.next {\n\t\ttop: auto;\n\t\tbottom: auto;\n\t\tleft: 50%;\n\t\tright: auto;\n\t\ttransform: translateX(-50%) rotate(90deg);\n\t}\n\n\t.previous {\n\t\ttop: $spacing-4;\n\t}\n\n\t.next {\n\t\tbottom: $spacing-4;\n\t}\n}\n"
    },
    {
      "path": "packages/react/src/components/carousel/index.ts",
      "type": "registry:ui",
      "target": "components/ui/carousel/index.ts",
      "content": "export {\n\tCarousel,\n\tCarouselContent,\n\tCarouselItem,\n\tCarouselPrevious,\n\tCarouselNext,\n\ttype CarouselProps,\n\ttype CarouselContentProps,\n\ttype CarouselItemProps,\n\ttype CarouselPreviousProps,\n\ttype CarouselNextProps,\n} from './carousel';\n"
    }
  ]
}
