diff --git a/common/changes/pcln-design-system/add-flashcard-component_2023-08-16-04-48.json b/common/changes/pcln-design-system/add-flashcard-component_2023-08-16-04-48.json
new file mode 100644
index 0000000000..1a3a20f611
--- /dev/null
+++ b/common/changes/pcln-design-system/add-flashcard-component_2023-08-16-04-48.json
@@ -0,0 +1,10 @@
+{
+ "changes": [
+ {
+ "packageName": "pcln-design-system",
+ "comment": "add Flashcard component",
+ "type": "minor"
+ }
+ ],
+ "packageName": "pcln-design-system"
+}
\ No newline at end of file
diff --git a/packages/core/src/Flashcard/Flashcard.spec.tsx b/packages/core/src/Flashcard/Flashcard.spec.tsx
new file mode 100644
index 0000000000..f18ca1c88a
--- /dev/null
+++ b/packages/core/src/Flashcard/Flashcard.spec.tsx
@@ -0,0 +1,69 @@
+import { render, screen } from '@testing-library/react'
+import React from 'react'
+import { Flashcard } from '..'
+
+const frontside = 'Front'
+const backside = 'Back'
+const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
+
+describe('Flashcard', () => {
+ it('renders the front content', () => {
+ render({frontside})
+ const front = screen.getByText(frontside)
+ const back = screen.queryByText(backside)
+ expect(front).toBeInTheDocument()
+ expect(back).not.toBeInTheDocument()
+ })
+
+ it('renders the back content when flipped', async () => {
+ render({frontside})
+
+ const front = screen.getByText(frontside)
+
+ front.click()
+ await wait(1000)
+
+ const front2 = screen.queryByText(frontside)
+ const back = screen.getByText(backside)
+ expect(front2).not.toBeInTheDocument()
+ expect(back).toBeInTheDocument()
+ })
+
+ it('renders the front content when dismissed', async () => {
+ const handleChange = jest.fn()
+
+ render(
+ <>
+ outside
+
+ {frontside}
+
+ >
+ )
+
+ const outside = screen.getByText('outside')
+ outside.click()
+ await wait(1000)
+
+ const front = screen.getByText(frontside)
+ const back = screen.queryByText(backside)
+ expect(front).toBeInTheDocument()
+ expect(back).not.toBeInTheDocument()
+ expect(handleChange).toHaveBeenCalled()
+ })
+
+ it('handles controlled state when open', async () => {
+ render(
+
+ {frontside}
+
+ )
+
+ const front = screen.getByText(frontside)
+ front.click()
+ await wait(1000)
+
+ const back = screen.queryByText(backside)
+ expect(back).not.toBeInTheDocument()
+ })
+})
diff --git a/packages/core/src/Flashcard/Flashcard.stories.args.tsx b/packages/core/src/Flashcard/Flashcard.stories.args.tsx
new file mode 100644
index 0000000000..7d59910598
--- /dev/null
+++ b/packages/core/src/Flashcard/Flashcard.stories.args.tsx
@@ -0,0 +1,30 @@
+import type { IFlashcardProps } from '..'
+import { borderRadii, Grid, paletteColors, shadows } from '..'
+
+import type { ArgTypes } from '@storybook/react'
+import React from 'react'
+import { flashCardRotations } from './Flashcard.styled'
+
+export const argTypes: Partial> = {
+ backside: { table: { disable: true } },
+ backsideBg: { control: { type: 'select' }, options: paletteColors },
+ bg: { control: { type: 'select' }, options: paletteColors },
+ borderRadius: { control: { type: 'select' }, options: Object.keys(borderRadii) },
+ boxShadowSize: { control: { type: 'select' }, options: Object.keys(shadows) },
+ children: { table: { disable: true } },
+ duration: { control: { type: 'number' } },
+ open: { control: { type: 'boolean' } },
+ perspective: { control: { type: 'number' } },
+ rotation: { control: { type: 'select' }, options: flashCardRotations },
+}
+
+export const defaultArgs: Partial = {
+ backside: Back,
+ backsideBg: 'secondary.light',
+ bg: 'primary.light',
+ borderRadius: 'xl',
+ children: Front,
+ duration: 0.5,
+ perspective: 200,
+ rotation: 'y',
+}
diff --git a/packages/core/src/Flashcard/Flashcard.stories.tsx b/packages/core/src/Flashcard/Flashcard.stories.tsx
new file mode 100644
index 0000000000..eb1ace2884
--- /dev/null
+++ b/packages/core/src/Flashcard/Flashcard.stories.tsx
@@ -0,0 +1,197 @@
+import type { Meta, StoryObj } from '@storybook/react'
+import { ArrowLeft } from 'pcln-icons'
+import React, { useState } from 'react'
+
+import type { IFlashcardProps, IGridProps } from '..'
+import { Button, Flashcard, Grid, IconButton, Text } from '..'
+import { argTypes, defaultArgs } from './Flashcard.stories.args'
+
+type FlashcardStory = StoryObj
+
+const ExampleImage = () => (
+
+)
+
+export const Playground: FlashcardStory = {
+ render: (args) => (
+
+ ,
+
+ ),
+}
+
+export const Multiple: FlashcardStory = {
+ render: (args) => (
+
+
+
+
+
+
+ ),
+}
+
+export const DifferentSizes: FlashcardStory = {
+ render: (args) => (
+
+ Back}>
+ Front
+
+
+ ),
+}
+
+const GridCell = (props: IGridProps) =>
+export const ContentShift: FlashcardStory = {
+ render: (args) => (
+
+ Top Left
+ Top
+ Top Right
+ Left
+
+
+ Back
+
+
+ Lorem ipsum dolor sit amet consectetur adipisicing elit.
+
+ }
+ >
+
+ Front
+
+
+
+ Right
+ Bottom Left
+ Bottom
+ Bottom Right
+
+ ),
+}
+
+export const OverflowContent: FlashcardStory = {
+ ...Playground,
+ args: {
+ ...defaultArgs,
+ children: (
+
+
+ Front
+
+ Lorem ipsum dolor, sit amet consectetur adipisicing elit. Dolore accusamus mollitia ipsa aliquid,
+ placeat sint consequatur inventore doloribus in culpa dolorum excepturi possimus ab. Deleniti
+ repellat quo aspernatur nihil. Maiores.
+
+
+
+ ),
+ backside: (
+
+
+ Back
+
+ Lorem ipsum dolor, sit amet consectetur adipisicing elit. Dolore accusamus mollitia ipsa aliquid,
+ placeat sint consequatur inventore doloribus in culpa dolorum excepturi possimus ab. Deleniti
+ repellat quo aspernatur nihil. Maiores.
+
+
+
+ ),
+ },
+}
+
+export const Image: FlashcardStory = {
+ ...Playground,
+ args: {
+ ...defaultArgs,
+ perspective: 400,
+ children: (
+
+
+
+ ),
+ backside: (
+
+
+
+
+
+ Beach
+
+ Lorem ipsum dolor, sit amet consectetur adipisicing elit. Dolore accusamus mollitia ipsa aliquid,
+ placeat sint consequatur inventore doloribus in culpa dolorum excepturi possimus ab. Deleniti
+ repellat quo aspernatur nihil. Maiores.
+
+
+ ),
+ },
+}
+
+export const Controlled: FlashcardStory = {
+ render: (args) => {
+ const [open, setOpen] = useState(false)
+
+ const flipButton = (
+ }
+ style={{ position: 'absolute', top: '1rem', left: '1rem', border: '2px solid currentcolor' }}
+ onClick={() => setOpen(!open)}
+ borderRadius='full'
+ />
+ )
+
+ return (
+
+
+ {flipButton} Back }>
+ {flipButton} Front
+
+
+ )
+ },
+ argTypes: { open: { table: { disable: true } } },
+ args: { perspective: 300 },
+}
+
+const insideProps = { width: 200, height: 200, placeItems: 'center', p: 3 }
+const outsideProps = { width: 300, height: 300, placeItems: 'center', p: 3 }
+export const Nested: FlashcardStory = {
+ render: (args) => (
+
+ Outside Back }>
+
+ Outside Front
+ Inside Back }>
+ Inside Front
+
+
+
+
+ ),
+ args: {
+ ...defaultArgs,
+ boxShadowSize: 'md',
+ },
+}
+
+const meta: Meta = {
+ title: 'Flashcard',
+ component: Flashcard,
+ args: defaultArgs,
+ argTypes: argTypes,
+}
+
+export default meta
diff --git a/packages/core/src/Flashcard/Flashcard.styled.tsx b/packages/core/src/Flashcard/Flashcard.styled.tsx
new file mode 100644
index 0000000000..a06b17f701
--- /dev/null
+++ b/packages/core/src/Flashcard/Flashcard.styled.tsx
@@ -0,0 +1,37 @@
+import type { IFlashcardProps } from '..'
+
+import themeGet from '@styled-system/theme-get'
+import type { ForwardRefComponent, HTMLMotionProps, TargetAndTransition } from 'framer-motion'
+import { motion } from 'framer-motion'
+import styled from 'styled-components'
+
+export const flashCardRotations = ['x', 'y', 'x-reverse', 'y-reverse'] as const
+export type FlashcardRotation = (typeof flashCardRotations)[number]
+
+export const flashcardRotations: Record = {
+ x: { rotateX: 180 },
+ y: { rotateY: 180 },
+ 'x-reverse': { rotateX: -180 },
+ 'y-reverse': { rotateY: -180 },
+ reset: { rotateX: 0, rotateY: 0 },
+} as const
+
+export type FlashcardMotionProps = Partial & HTMLMotionProps<'div'>
+
+export type FlashcardContainerProps = HTMLMotionProps<'div'> &
+ Partial>
+
+export const CardContainer: (props: FlashcardContainerProps) => JSX.Element = styled(motion.div)`
+ perspective: ${(props: FlashcardContainerProps) => props.perspective}px;
+`
+
+export const OuterCardMotion: ForwardRefComponent = styled(motion.div)``
+
+export const InnerCardMotion: ForwardRefComponent = styled(motion.div)`
+ background-color: ${(props: FlashcardMotionProps) => themeGet(`palette.${props.bg}`)(props)};
+ border-radius: ${(props: FlashcardMotionProps) => themeGet(`borderRadii.${props.borderRadius}`)(props)};
+ box-shadow: ${(props: FlashcardMotionProps) => themeGet(`shadows.${props.boxShadowSize}`)(props)};
+ &:hover {
+ box-shadow: ${(props: FlashcardMotionProps) => themeGet(`shadows.2xl`)(props)};
+ }
+`
diff --git a/packages/core/src/Flashcard/Flashcard.tsx b/packages/core/src/Flashcard/Flashcard.tsx
new file mode 100644
index 0000000000..074688e230
--- /dev/null
+++ b/packages/core/src/Flashcard/Flashcard.tsx
@@ -0,0 +1,98 @@
+import type { BorderRadius, BoxShadowSize, PaletteColor } from '..'
+import {
+ CardContainer,
+ FlashcardRotation,
+ InnerCardMotion,
+ OuterCardMotion,
+ flashcardRotations,
+} from './Flashcard.styled'
+
+import React, { useEffect, useRef, useState } from 'react'
+
+export interface IFlashcardProps {
+ backside: React.ReactNode
+ backsideBg?: PaletteColor
+ bg?: PaletteColor
+ borderRadius?: BorderRadius
+ boxShadowSize?: BoxShadowSize
+ children: React.ReactNode
+ defaultOpen?: boolean
+ duration?: number
+ onOpenChange?: (open: boolean) => void
+ open?: boolean
+ perspective?: number | string
+ rotation?: FlashcardRotation
+}
+
+const Flashcard = ({
+ backside,
+ backsideBg,
+ bg,
+ borderRadius = 'xl',
+ boxShadowSize,
+ children,
+ defaultOpen = false,
+ duration = 0.5,
+ onOpenChange,
+ open,
+ perspective = 200,
+ rotation = 'y',
+}: IFlashcardProps) => {
+ const [_open, setOpen] = useState(open ?? defaultOpen)
+ const [_bg, setBg] = useState(_open ? backsideBg : bg)
+ const [_children, setChildren] = useState(_open ? backside : children)
+ const ref = useRef(null)
+
+ useEffect(() => setChildren(_open ? backside : children), [children, backside])
+ useEffect(() => setBg(_open ? backsideBg : bg), [bg, backsideBg])
+
+ const handleOpenChange = (newOpen: boolean) => {
+ onOpenChange?.(newOpen)
+ setOpen(newOpen)
+ setTimeout(() => {
+ setChildren(newOpen ? backside : children)
+ setBg(newOpen ? backsideBg : bg)
+ }, (duration * 1000) / 2)
+ }
+
+ useEffect(() => handleOpenChange(open), [open])
+
+ useEffect(() => {
+ if (open !== undefined) return
+ const handleOutsideClick = (event: MouseEvent) => {
+ if (ref.current && !ref.current.contains(event.target)) handleOpenChange(false)
+ }
+ document.addEventListener('mousedown', handleOutsideClick)
+ return () => document.removeEventListener('mousedown', handleOutsideClick)
+ }, [ref])
+
+ return (
+
+
+ {
+ e.stopPropagation()
+ handleOpenChange(true)
+ }
+ : undefined
+ }
+ ref={ref}
+ transition={{ duration }}
+ >
+ {_children}
+
+
+
+ )
+}
+
+export default Flashcard
diff --git a/packages/core/src/Flashcard/index.ts b/packages/core/src/Flashcard/index.ts
new file mode 100644
index 0000000000..6be4be21fe
--- /dev/null
+++ b/packages/core/src/Flashcard/index.ts
@@ -0,0 +1,3 @@
+export type { IFlashcardProps } from './Flashcard'
+
+export { default as Flashcard } from './Flashcard'
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index 216dcb6cd4..88a607dc9d 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -2,6 +2,7 @@ import * as storybookArgs from './storybook/args'
export type { IButtonProps } from './Button'
export type { IDialogProps } from './Dialog'
+export type { IFlashcardProps } from './Flashcard'
export type { IGridProps } from './Grid'
export { Absolute } from './Absolute'
@@ -24,6 +25,7 @@ export { Dialog } from './Dialog'
export { Divider } from './Divider'
export { DotLoader } from './DotLoader'
export { Flag } from './Flag'
+export { Flashcard } from './Flashcard'
export { Flex } from './Flex'
export { FormField, InputField } from './FormField'
export { GenericBanner } from './GenericBanner'
diff --git a/packages/core/src/theme/theme.ts b/packages/core/src/theme/theme.ts
index 84e013d2ea..d6f05a34a6 100644
--- a/packages/core/src/theme/theme.ts
+++ b/packages/core/src/theme/theme.ts
@@ -312,6 +312,8 @@ export const shadows = {
'0 -1px 0 0 rgba(0,0,0,0.03),0 24px 72px 0 rgba(0,0,0,0.48),0 8px 16px 0 rgba(0,0,0,0.12),0 24px 64px 0 rgba(0,0,0,0.2)',
}
+export type BoxShadowSize = keyof typeof shadows
+
export const textShadows = {
sm: `0 1px 2px rgba(0,0,0,0.5)`,
md: `0 2px 4px rgba(0,0,0,0.5)`,