Repository: material-ui
Issue: [joy-ui] Add Carousel component
Labels: new feature,waiting for 👍,package: joy-ui

### Duplicates

- [X] I have searched the existing issues

### Latest version

- [X] I have tested the latest version

### Summary 💡

@siriwatknp @danilo-leal 

So standard Material UI doesn't provide any carousel component out of the box, however after playing around with the new Joy UI components I started writing a CSS snap carousel component that could possibly be useful for Joy. The idea partially stems from the Google news feed and a few other apps I commonly use.

I'll provide the src files below as this isn't a ready component in any way, it still doesn't have the forward back scroll functions in place, a lot of the type names need to be fixed, and I'm unsure if the virtual library I use is appropriate for this. It definitely needs a bit of work but I'd be really happy to hear people's thoughts on how a carousel component should be designed.

**BUGS**
So far the only bug I've run into is when you scroll to the last element, the first element in the list beings to render, then disappear in a loop. I honestly don't know if virtual makes sense for this, however major carousel libraries have this kind of support and there are cases where it could be needed. Perhaps you are rendering a gallery then it could make sense to have, images that wouldn't all be loaded at once, etc. 

**NOTES**
There is also a prop called isSSR to switch between standard rendering and using the virtual hook (@tanstack/react-virtual), I still need to devise a solution to allow users to programmatically scroll regardless of the method used.

(Also the file structure is slightly dif from MUI. Would be fixed later.)
**Index**
<details>
<summary>Carousel Root w/ Virtual Hook</summary>
<p>

```
import { mergeRefs } from '@fox-dls/utils';
import { unstable_composeClasses as composeClasses } from '@mui/base';
import { Box, Button, useThemeProps } from '@mui/material';
import { OverridableComponent } from '@mui/types';
import clsx from 'clsx';
import PropTypes from 'prop-types';
import * as React from 'react';
import { CarouselContext } from './context';
import { CarouselRoot } from './styles';
import {
  getCarouselUtilityClass,
  CarouselProps,
  CarouselTypeMap
} from './types';
import { useVirtualizer } from '@tanstack/react-virtual';

const useUtilityClasses = (ownerState: CarouselProps) => {
  const slots = {
    root: []
  };

  return composeClasses(slots, getCarouselUtilityClass, {});
};

export const Carousel = React.forwardRef(function Carousel(inProps, ref) {
  const props = useThemeProps({
    props: inProps,
    name: 'MuiCarousel'
  });

  const {
    className,
    //@ts-ignore
    component = 'div',
    children,
    visibleItems,
    gap,
    isControls,
    vertical,
    viewConfig,
    placeholder,
    isSSR,
    ...other
  } = props;

  const ownerState = {
    ...props,
    component
  };

  const classes = useUtilityClasses(ownerState);

  const scrollerRef = React.useRef();

  const scrollerVirtualizer = useVirtualizer({
    horizontal: vertical ? false : true,
    count: React.Children.count(children),
    getScrollElement: () => scrollerRef.current,
    estimateSize: () => 0,
    ...viewConfig
  });

  return (
    <CarouselContext.Provider value={null}>
      <CarouselRoot
        as={component}
        ownerState={ownerState}
        className={clsx(classes.root, className)}
        ref={mergeRefs([ref, scrollerRef])}
        {...other}
      >
        {isSSR
          ? scrollerVirtualizer.getVirtualItems().map(virtual => (
              <Box key={virtual.index} ref={virtual.measureElement}>
                {placeholder
                  ? React.Children.toArray(children)[virtual.index] ??
                    placeholder
                  : React.Children.toArray(children)[virtual.index]}
              </Box>
            ))
          : React.Children.map(children, (child, index) => {
              if (!React.isValidElement(child)) {
                return child;
              }
              if (index === 0) {
                return React.cloneElement(child, {
                  'carousel-first-child': ''
                });
              }
              if (index === React.Children.count(children) - 1) {
                return React.cloneElement(child, { 'carousel-last-child': '' });
              }
              return child;
            })}
      </CarouselRoot>
      <button onClick={() => scrollerVirtualizer.scrollToIndex(4)}>
        Scroll To Index 2
      </button>
    </CarouselContext.Provider>
  );
}) as OverridableComponent<CarouselTypeMap>;

Carousel.propTypes /* remove-proptypes */ = {
  /**
   * Used to render icon or text elements inside the Carousel if `src` is not set.
   * This can be an element, or just a string.
   */
  children: PropTypes.node,
  /**
   * @ignore
   */
  className: PropTypes.string,
  /**
   * The component used for the root node.
   * Either a string to use a HTML element or a component.
   */
  component: PropTypes.elementType,
  /**
   * The number of items visible at a given time. Defaults to 1
   */
  visibleItems: PropTypes.number,
  /**
   * Item peek in px
   */
  peek: PropTypes.string,
  /**
   * The gap between items in px. Defaults to 0
   */
  gap: PropTypes.string,
  /**
   * Is Verical or Horizontal
   */
  vertical: PropTypes.bool,
  /**
   * Config for view observer
   */
  viewConfig: PropTypes.object,
  /**
   * Children props
   */
  itemProps: PropTypes.shape({}),
  /**
   * Placeholder node to load
   */
  placeholder: PropTypes.node,
  /**
   * Is SSR
   */
  isSSR: PropTypes.bool,
  sx: PropTypes.oneOfType([
    PropTypes.arrayOf(
      PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])
    ),
    PropTypes.func,
    PropTypes.object
  ])
} as any;
```
</p>
</details>

**Types & Classes**
<details>
<summary>Types & Classes</summary>
<p>

```
import * as React from 'react';
import { OverridableStringUnion, OverrideProps } from '@mui/types';
import { SxProps } from '@mui/system';
import { generateUtilityClass, generateUtilityClasses } from '@mui/base';

export type CarouselSlot = 'root';

export interface CarouselTypeMap<P = {}, D extends React.ElementType = 'div'> {
  props: P & {
    /**
     * Used to render icon or text elements inside the Carousel if `src` is not set.
     * This can be an element, or just a string.
     */
    children?: React.ReactNode;
    /**
     * The system prop that allows defining system overrides as well as additional CSS styles.
     */
    sx?: SxProps;
    /**
     * The number of items visible at a given time. Defaults to 1
     */
    visibleItems?: number;
    /**
     * Item peek in px
     */
    peek?: string;
    /**
     * The gap between items in px. Defaults to 0
     */
    gap?: string;
    /**
     * Are the overlay controls available
     */
    isControls?: boolean;
    /**
     * Is Horizontal or Vertical
     */
    vertical?: boolean;
    /**
     * Config for view observer
     */
    viewConfig?: object;
    /**
     * Placeholder node to load
     */
    placeholder?: React.ReactNode;
    /**
     * Is SSR
     */
    isSSR?: boolean;
  };
  defaultComponent: D;
}

export type CarouselProps<
  D extends React.ElementType = CarouselTypeMap['defaultComponent'],
  P = { component?: React.ElementType }
> = OverrideProps<CarouselTypeMap<P, D>, D>;

export interface CarouselClasses {
  /** Styles applied to the root element. */
  root: string;
}

export type CarouselClassKey = keyof CarouselClasses;

export function getCarouselUtilityClass(slot: string): string {
  return generateUtilityClass('MuiCarousel', slot);
}

const CarouselClasses: CarouselClasses = generateUtilityClasses('MuiCarousel', [
  'root'
]);

export default CarouselClasses;
```
</p>
</details>

**Styles**
<details>
<summary>Carousel Style</summary>
<p>

```
import { styled } from '@mui/system';
import { CarouselProps } from '../types';
import { resolveSxValue } from '../../theme/mui/utils';

export const CarouselRoot = styled('div', {
  name: 'MuiCarousel',
  slot: 'Root',
  overridesResolver: (props, styles) => styles.root
})<{ theme?: any; ownerState: CarouselProps }>(({ theme, ownerState }) => [
  {
    '--Carousel-contain': 'layout paint size style',
    '--Carousel-overflow': 'visible hidden',
    '--Carousel-content-visibility': 'unset',
    '--Carousel-controls-z-index': 1,
    '--Carousel-gap': ownerState.gap ?? '0px',
    '--Carousel-height': '100%',
    '--Carousel-peek': ownerState.peek ?? '0px',
    '--Carousel-scrollsnapstop': 'always',
    '--Carousel-visible-items': ownerState.visibleItems ?? 1,

    '&::-webkit-scrollbar': {
      display: 'none'
    },

    '& > *': {
      scrollSnapAlign: 'start'
    },

    ['@supports (-webkit-scroll-behavior:smooth) or (-moz-scroll-behavior:smooth) or (-ms-scroll-behavior:smooth) or (scroll-behavior:smooth)']:
      {
        scrollSnapType: 'var(--Carousel-scroll-snap-type,inline mandatory);'
      },
    margin: '0px !important',
    width: '100% !important',
    height: 'var(--Carousel-height)',
    contain: 'var(--Carousel-contain)',
    containIntrinsicSize: 'var(--Carousel-contain-intrinsic)',
    //contentVisibility: 'var(--Carousel-content-visibility)',
    display: 'grid',
    gridAutoFlow: 'column',
    gridAutoColumns:
      'var( --Carousel-auto-columns, calc( ( 100% - var(--Carousel-gap,16px) * (var(--Carousel-visible-items,unset) - 1) ) / var(--Carousel-visible-items,unset) ) )',
    gap: 'var(--Carousel-gap,16px)',
    gridTemplateRows: 'var(--Carousel-rows,none)',
    justifyContent: 'flex-start',
    minHeight: 'var(--Carousel-min-height)',
    overflow: 'var(--Carousel-overflow,auto hidden)',
    overscrollBehaviorInline: 'contain',
    paddingBlockStart: 'var(--Carousel-padding-block-start,unset)',
    paddingBlockEnd: 'var(--Carousel-padding-block-end,unset)',
    paddingInlineStart:
      'var( --Carousel-padding-inline-start, var(--Carousel-peek,32px) )',
    paddingInlineEnd:
      'var( --Carousel-padding-inline-end, var(--Carousel-peek,32px) )',
    scrollPaddingInline: 'var(--Carousel-peek,32px)',
    touchAction: 'var(--Carousel-touch-action,pan-x pan-y pinch-zoom)',
    scrollbarWidth: 'none'
  }
]);
```
</p>
</details>

### Examples 🌈

I still need to move all this to its own sandbox.

```
<AspectRatio ratio="20/19" sx={{ width: '100%', height: '100%' }}>
        <CardContainer>
          <CardContent>
            <Carousel
              gap="0px"
              visibleItems={2}
              gap="10px"
              placeholder={<Loading />}
              isSSR={true}
            >
              <CardContainer sx={{ height: '100%', borderRadius: '0px' }}>
                <CardCover>
                  <img
                    src="https://images.unsplash.com/photo-1656536665880-2cb1ab7d54e3?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=774&q=80"
                    alt=""
                  />
                </CardCover>
              </CardContainer>
              <CardContainer sx={{ height: '100%', borderRadius: '0px' }}>
                <CardCover>
                  <img
                    src="https://images.unsplash.com/photo-1656231934649-c476090deb59?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1524&q=80"
                    alt=""
                  />
                </CardCover>
              </CardContainer>
              <CardContainer sx={{ height: '100%', borderRadius: '0px' }}>
                <CardCover>
                  <img
                    src="https://images.unsplash.com/photo-1656501854040-2850f4a37562?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=774&q=80"
                    alt=""
                  />
                </CardCover>
              </CardContainer>
              <CardContainer sx={{ height: '100%', borderRadius: '0px' }}>
                <CardCover>
                  <img
                    src="https://images.unsplash.com/photo-1656501854040-2850f4a37562?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=774&q=80"
                    alt=""
                  />
                </CardCover>
              </CardContainer>
            </Carousel>
          </CardContent>
        </CardContainer>
      </AspectRatio>
```

### Motivation 🔦

It looks to me as if Joy might be the next big thing for MUI and I would like to attempt to provide useful components so that other developers will be able to make cool things!