@microlink/react
Version:
Turn links into beautiful previews.
1 lines • 105 kB
Source Map (JSON)
{"version":3,"file":"microlink.min.cjs","sources":["../../../node_modules/.pnpm/nanoclamp@2.0.7_react@18.2.0/node_modules/nanoclamp/dist/nanoclamp.mjs","../../../node_modules/.pnpm/is-local-address@2.0.0/node_modules/is-local-address/src/index.js","../../../node_modules/.pnpm/is-local-address@2.0.0/node_modules/is-local-address/src/ipv4.js","../../../node_modules/.pnpm/is-local-address@2.0.0/node_modules/is-local-address/src/ipv6.js","../src/utils/index.js","../src/components/Card/CardText.js","../src/theme.js","../src/context/GlobalState.js","../src/components/Card/CardContent.js","../src/components/Card/CardAnimation.js","../src/components/Card/CardMedia/loader.js","../src/components/Card/CardMedia/Wrap.js","../src/components/Card/CardMedia/Image.js","../src/components/Card/CardEmpty.js","../src/components/Card/CardMedia/Controls/MediaButton.js","../src/components/Card/CardMedia/Controls/FooterControls.js","../src/components/Card/CardMedia/Controls/PlaybackButton.js","../src/components/Card/CardMedia/Controls/ProgressBar/Scrubber.js","../src/components/Card/CardMedia/Controls/ProgressBar/Tooltip.js","../src/components/Card/CardMedia/Controls/ProgressBar/index.js","../src/components/Card/CardMedia/Controls/SeekButton.js","../src/components/Card/CardMedia/Controls/Spinner.js","../src/components/Card/CardMedia/Controls/index.js","../src/components/Card/CardMedia/Video.js","../src/components/Card/CardMedia/index.js","../src/components/Card/CardMedia/Audio.js","../src/components/Card/CardWrap.js","../src/index.js","../src/utils/hooks.js"],"sourcesContent":["import{useRef as e,useMemo as t,useCallback as n,useEffect as r,createElement as i}from\"react\";const l=({accessibility:l=!0,debounce:o=300,ellipsis:u=\"…\",is:c=\"div\",lines:s=3,text:d,...v})=>{const a=e(null),f=e(\".\"),h={ref:a,...l?{title:d}:{},...v},g=t((()=>\"string\"==typeof d&&d.length>0),[d]),m=n((()=>{var e,t,n,r;if(!g)return;const i=e=>{f.current=e,null!=a.current&&(a.current.innerText=e)};i(\".\");const l=((null!==(t=null===(e=a.current)||void 0===e?void 0:e.clientHeight)&&void 0!==t?t:0)+1)*s+1,o=\"…\"===u?5:1.2*u.length;let c=0,v=0,h=d.length;for(;c<=h;){v=Math.floor((c+h)/2);if(i(d.slice(0,v)),v===d.length)return;(null!==(r=null===(n=a.current)||void 0===n?void 0:n.clientHeight)&&void 0!==r?r:0)<=l?c=v+1:h=v-1}i(d.slice(0,Math.max(v-o,0)).trim()+u)}),[u,g,s,d]);return r((()=>{m();const e=((e,t)=>{let n;const r=()=>{n=void 0,e()};return()=>{const i=null==n;clearTimeout(n),n=setTimeout(r,t),i&&e()}})(m,o);return window.addEventListener(\"resize\",e),()=>window.removeEventListener(\"resize\",e)}),[m,o]),g?i(c,h,f.current):null};export{l as default};\n//# sourceMappingURL=nanoclamp.mjs.map\n","'use strict'\nmodule.exports = hostname => require('./ipv4')(hostname) || require('./ipv6')(hostname)\n","'use strict'\n\nconst IP_RANGES = [\n // 10.0.0.0 - 10.255.255.255\n /^(:{2}f{4}:)?10(?:\\.\\d{1,3}){3}$/,\n // 127.0.0.0 - 127.255.255.255\n /^(:{2}f{4}:)?127(?:\\.\\d{1,3}){3}$/,\n // 169.254.1.0 - 169.254.254.255\n /^(::f{4}:)?169\\.254\\.([1-9]|1?\\d\\d|2[0-4]\\d|25[0-4])\\.\\d{1,3}$/,\n // 172.16.0.0 - 172.31.255.255\n /^(:{2}f{4}:)?(172\\.1[6-9]|172\\.2\\d|172\\.3[01])(?:\\.\\d{1,3}){2}$/,\n // 192.168.0.0 - 192.168.255.255\n /^(:{2}f{4}:)?192\\.168(?:\\.\\d{1,3}){2}$/,\n // fc00::/7\n /^f[cd][\\da-f]{2}(::1$|:[\\da-f]{1,4}){1,7}$/,\n // fe80::/10s\n /^fe[89ab][\\da-f](::1$|:[\\da-f]{1,4}){1,7}$/,\n // localhost in IPv4\n /^localhost$|^0\\.0\\.0\\.0$/\n]\n\nconst regex = new RegExp(`^(${IP_RANGES.map(re => re.source).join('|')})$`)\n\nmodule.exports = regex.test.bind(regex)\nmodule.exports.regex = regex\n","'use strict'\n\nconst IP_RANGES = [\n // localhost in IPv6\n /^\\[(::1|::)\\]$/\n]\n\nconst regex = new RegExp(`^(${IP_RANGES.map(re => re.source).join('|')})$`)\n\nmodule.exports = regex.test.bind(regex)\nmodule.exports.regex = regex\n","import { fetchFromApi, getApiUrl as createApiUrl } from '@microlink/mql'\nimport isLocalAddress from 'is-local-address'\nimport { css } from 'styled-components'\n\nexport const isSSR = typeof window === 'undefined'\n\nexport const castArray = value => [].concat(value)\n\nexport const getPreferredMedia = (data, mediaProps) => {\n let prefer\n\n for (let index = 0; index < mediaProps.length; index++) {\n const key = mediaProps[index]\n const value = data[key]\n if (!isNil(value)) {\n prefer = key\n break\n }\n }\n\n return prefer\n}\n\nexport const isFunction = fn => typeof fn === 'function'\n\nexport const isObject = obj => typeof obj === 'object'\n\nexport const isNil = value => value == null\n\nexport const getUrlPath = data => (isObject(data) ? data.url : data)\n\nexport const someProp = (data, props) =>\n data[props.find(prop => !isNil(data[prop]))]\n\nexport const media = {\n mobile: (...args) => css`\n @media (max-width: 48em) {\n ${css(...args)};\n }\n `,\n desktop: (...args) => css`\n @media (min-width: 48em) {\n ${css(...args)};\n }\n `\n}\n\nexport const getApiUrl = ({\n apiKey,\n contrast = false,\n data,\n endpoint,\n force,\n headers,\n media,\n prerender,\n proxy,\n ttl,\n url\n}) =>\n createApiUrl(url, {\n apiKey,\n audio: media.includes('audio'),\n data,\n endpoint,\n force,\n headers,\n iframe: media.includes('iframe'),\n palette: contrast,\n prerender,\n proxy,\n screenshot: media.includes('screenshot'),\n ttl,\n video: media.includes('video')\n })\n\nexport { fetchFromApi }\n\nexport const isLarge = cardSize => cardSize === 'large'\n\nexport const isSmall = cardSize => cardSize === 'small'\n\nexport const imageProxy = url =>\n isLocalAddress(new URL(url).hostname)\n ? url\n : `https://images.weserv.nl/?${new URLSearchParams({\n url,\n default: url,\n l: 9,\n af: '',\n il: '',\n n: -1\n }).toString()}`\n\nexport const isLazySupported = !isSSR && 'IntersectionObserver' in window\n\nexport const formatSeconds = secs => {\n const secsToNum = parseInt(secs, 10)\n const hours = Math.floor(secsToNum / 3600)\n const minutes = Math.floor(secsToNum / 60) % 60\n const seconds = secsToNum % 60\n\n return [hours, minutes, seconds]\n .filter((v, i) => v > 0 || i > 0)\n .map(v => (v >= 10 ? v : `0${v}`))\n .join(':')\n}\n\nexport const clampNumber = (num, min, max) => {\n switch (true) {\n case num <= min:\n return min\n case num >= max:\n return max\n default:\n return num\n }\n}\n\nconst BASE_CLASSNAME = 'microlink_card'\nconst CONTENT_BASE_CLASSNAME = `${BASE_CLASSNAME}__content`\nconst MEDIA_BASE_CLASSNAME = `${BASE_CLASSNAME}__media`\nconst CONTROLS_BASE_CLASSNAME = `${MEDIA_BASE_CLASSNAME}__controls`\n\nexport const classNames = {\n main: BASE_CLASSNAME,\n content: CONTENT_BASE_CLASSNAME,\n title: `${CONTENT_BASE_CLASSNAME}_title`,\n description: `${CONTENT_BASE_CLASSNAME}_description`,\n url: `${CONTENT_BASE_CLASSNAME}_url`,\n mediaWrapper: `${MEDIA_BASE_CLASSNAME}_wrapper`,\n media: MEDIA_BASE_CLASSNAME,\n image: `${MEDIA_BASE_CLASSNAME}_image`,\n videoWrapper: `${MEDIA_BASE_CLASSNAME}_video_wrapper`,\n video: `${MEDIA_BASE_CLASSNAME}_video`,\n audioWrapper: `${MEDIA_BASE_CLASSNAME}_audio_wrapper`,\n audio: `${MEDIA_BASE_CLASSNAME}_audio`,\n mediaControls: CONTROLS_BASE_CLASSNAME,\n playbackControl: `${CONTROLS_BASE_CLASSNAME}_playback`,\n volumeControl: `${CONTROLS_BASE_CLASSNAME}_volume`,\n rwControl: `${CONTROLS_BASE_CLASSNAME}_rewind`,\n ffwControl: `${CONTROLS_BASE_CLASSNAME}_fast_forward`,\n rateControl: `${CONTROLS_BASE_CLASSNAME}_rate`,\n progressBar: `${CONTROLS_BASE_CLASSNAME}_progress_bar`,\n progressTime: `${CONTROLS_BASE_CLASSNAME}_progress_time`,\n spinner: `${CONTROLS_BASE_CLASSNAME}_spinner`,\n iframe: `${BASE_CLASSNAME}__iframe`\n}\n","/* eslint-disable multiline-ternary */\n\nimport React from 'react'\nimport { styled, css } from 'styled-components'\nimport NanoClamp from 'nanoclamp'\n\nimport { isNil } from '../../utils'\n\nconst Clamp = ({ children, className, lines }) =>\n isNil(children) ? null : (\n <NanoClamp className={className} lines={lines} text={children} is='p' />\n )\n\nconst StyledClamp = styled(Clamp)`\n &&& {\n text-align: inherit;\n font-weight: inherit;\n font-family: inherit;\n color: inherit;\n margin: 0;\n\n ${({ $useNanoClamp }) =>\n !$useNanoClamp &&\n css`\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n `}\n }\n`\n\nconst CardText = ({ useNanoClamp = true, children, ...props }) => {\n const textProps = useNanoClamp\n ? props\n : { ...props, as: 'p', title: children }\n\n return (\n <StyledClamp $useNanoClamp={useNanoClamp} {...textProps}>\n {children}\n </StyledClamp>\n )\n}\n\nexport default CardText\n","export const speed = {\n short: '100ms',\n medium: '150ms',\n long: '300ms'\n}\n\nexport const animation = {\n short: 'cubic-bezier(.25,.8,.25,1)',\n medium: 'cubic-bezier(.25,.8,.25,1)',\n long: 'cubic-bezier(.4, 0, .2, 1)'\n}\n\nconst createTransition = (properties, s) => {\n const suffix = `${speed[s]} ${animation[s]}`\n return properties.map(property => `${property} ${suffix}`).join(', ')\n}\n\nexport const transition = {\n short: (...properties) => createTransition(properties, 'short'),\n medium: (...properties) => createTransition(properties, 'medium'),\n long: (...properties) => createTransition(properties, 'long')\n}\n\n// https://primer.style/design/foundations/typography\nexport const font = {\n sans: \"InterUI, -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', Oxygen, Ubuntu, Cantarell, 'Open Sans', sans-serif\",\n mono: \"'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace\"\n}\n","import React, { useCallback, useMemo, useState } from 'react'\n\nconst initialState = {}\n\nexport const GlobalContext = React.createContext(initialState)\n\nconst GlobalState = ({\n autoPlay,\n children,\n controls,\n loop,\n mediaRef,\n muted,\n playsInline,\n size,\n ...rest\n}) => {\n const [state, setState] = useState(initialState)\n\n const updateState = useCallback(\n newState => setState(currentState => ({ ...currentState, ...newState })),\n []\n )\n\n const props = useMemo(\n () => ({\n autoPlay,\n controls,\n loop,\n mediaRef,\n muted,\n playsInline,\n size\n }),\n [autoPlay, controls, loop, mediaRef, muted, playsInline, size]\n )\n\n const values = useMemo(\n () => ({\n props,\n state,\n updateState\n }),\n [props, state, updateState]\n )\n\n return (\n <GlobalContext.Provider value={values}>\n {children(rest)}\n </GlobalContext.Provider>\n )\n}\n\nexport default GlobalState\n","/* global URL */\n\nimport React, { useCallback, useMemo, useContext } from 'react'\nimport { styled, css } from 'styled-components'\nimport CardText from './CardText'\n\nimport { transition } from '../../theme'\nimport { classNames, media, isLarge, isSmall, isNil } from '../../utils'\nimport { GlobalContext } from '../../context/GlobalState'\n\nconst REGEX_STRIP_WWW = /^www\\./\nconst BADGE_WIDTH = '16px'\nconst BADGE_HEIGHT = '12px'\n\nconst getHostname = href => {\n if (isNil(href)) return ''\n const { hostname } = new URL(href)\n return hostname.replace(REGEX_STRIP_WWW, '')\n}\n\nconst mobileDescriptionStyle = css`\n ${media.mobile`\n > p {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n `};\n`\n\nexport const Content = styled('div').attrs({ className: classNames.content })`\n display: flex;\n padding: 10px 15px;\n min-width: 0;\n box-sizing: border-box;\n ${({ $cardSize }) => css`\n flex: ${!isLarge($cardSize) ? 1 : '0 0 125px'};\n justify-content: ${!isSmall($cardSize) ? 'space-around' : 'space-between'};\n flex-direction: ${!isSmall($cardSize) ? 'column' : 'row'};\n align-items: ${!isSmall($cardSize) ? 'stretch' : 'center'};\n `};\n`\n\nconst Header = styled('header').attrs({ className: classNames.title })`\n text-align: left;\n font-weight: bold;\n margin: 0;\n width: 100%;\n ${({ $cardSize }) => css`\n flex-grow: ${!isSmall($cardSize) ? 1.2 : 0.8};\n font-size: ${!isSmall($cardSize) ? '16px' : '15px'};\n\n ${isSmall($cardSize) &&\n css`\n min-width: 0;\n padding-right: 14px;\n `}\n `}\n`\n\nconst Description = styled('div').attrs({ className: classNames.description })`\n text-align: left;\n font-size: 14px;\n flex-grow: 2;\n margin: auto 0;\n line-height: 18px;\n font-weight: normal;\n ${({ $cardSize }) => !isLarge($cardSize) && mobileDescriptionStyle};\n`\n\nconst Footer = styled('footer').attrs({ className: classNames.url })`\n display: flex;\n align-items: center;\n justify-content: space-between;\n text-align: left;\n margin: 0;\n flex-grow: 0;\n font-weight: normal;\n ${({ $cardSize }) => css`\n font-size: ${!isSmall($cardSize) ? '12px' : '10px'};\n ${!isSmall($cardSize) && 'width: 100%;'}\n `};\n`\n\nconst Author = styled(CardText)`\n opacity: 0.75;\n transition: ${transition.medium('opacity')};\n will-change: opacity;\n\n .${classNames.main}:hover & {\n opacity: 1;\n }\n`\n\nconst PoweredBy = styled('span').attrs({ title: 'microlink.io' })`\n background: url('https://cdn.microlink.io/logo/logo.svg') no-repeat center\n center;\n display: block;\n margin-left: 15px;\n transition: ${transition.medium('filter', 'opacity')};\n will-change: filter, opacity;\n &:not(:hover) {\n filter: grayscale(100%);\n opacity: 0.75;\n }\n\n min-width: ${BADGE_WIDTH};\n width: ${BADGE_WIDTH};\n background-size: ${BADGE_WIDTH};\n height: ${BADGE_HEIGHT};\n`\n\nconst CardContent = () => {\n const {\n state: { description, title, url },\n props: { size }\n } = useContext(GlobalContext)\n const isSmallCard = isSmall(size)\n const formattedUrl = useMemo(() => getHostname(url), [url])\n const handleOnClick = useCallback(e => {\n e.preventDefault()\n window.open('https://www.microlink.io', '_blank')\n }, [])\n\n return (\n <Content $cardSize={size}>\n <Header $cardSize={size}>\n <CardText $useNanoClamp={false}>{title}</CardText>\n </Header>\n {!isSmallCard && (\n <Description $cardSize={size}>\n <CardText lines={2}>{description}</CardText>\n </Description>\n )}\n <Footer $cardSize={size}>\n <Author $useNanoClamp={false}>{formattedUrl}</Author>\n <PoweredBy onClick={handleOnClick} />\n </Footer>\n </Content>\n )\n}\n\nexport default CardContent\n","import { css, keyframes } from 'styled-components'\n\nconst emptyStatePulse = keyframes`\n 0% {\n background: #e1e8ed;\n }\n 70% {\n background: #cdd4d8;\n }\n 100% {\n background: #e1e8ed;\n }\n`\nconst emptyStateImagePulse = keyframes`\n 0% {\n background: #e1e8ed;\n }\n 70% {\n background: #dce3e8;\n }\n 100% {\n background: #e1e8ed;\n }\n`\n\nexport const emptyStateAnimation = css`\n animation: ${emptyStatePulse} .75s linear infinite;\n`\n\nexport const emptyStateImageAnimation = css`\n animation: ${emptyStateImagePulse} 1.25s linear infinite;\n`\n","import { styled, css } from 'styled-components'\nimport { transition } from '../../../theme'\n\nexport const ImageLoadCatcher = styled('img')`\n height: 1px;\n width: 1px;\n position: absolute;\n z-index: -1;\n`\n\nexport const loadingOverlay = css`\n &::after {\n content: '';\n position: absolute;\n left: 0;\n top: 0;\n right: 0;\n bottom: 0;\n background: #e1e8ed;\n z-index: 1;\n transition: ${transition.medium('opacity', 'visibility')};\n will-change: opacity;\n\n ${({ $isLoading }) => css`\n opacity: ${$isLoading ? 1 : 0};\n visibility: ${$isLoading ? '$visible' : 'hidden'};\n `};\n }\n`\n","import React, { useContext } from 'react'\nimport { styled, css } from 'styled-components'\n\nimport { GlobalContext } from '../../../context/GlobalState'\nimport { media } from '../../../utils'\nimport { loadingOverlay } from './loader'\n\nconst mediaSizeStyles = {\n small: css`\n flex: 0 0 48px;\n `,\n normal: css`\n flex: 0 0 125px;\n\n ${media.mobile`\n flex: 0 0 92px;\n `}\n `,\n large: css`\n flex: 1;\n\n &::before {\n padding-bottom: 0;\n }\n `\n}\n\nconst StyledWrap = styled('div')`\n background: transparent no-repeat center center / cover;\n display: block;\n overflow: hidden;\n height: auto;\n position: relative;\n\n &::before {\n content: '';\n padding-bottom: 100%;\n display: block;\n }\n\n ${({ $cardSize }) => mediaSizeStyles[$cardSize]};\n\n ${loadingOverlay};\n`\n\nconst Wrap = props => {\n const {\n props: { size }\n } = useContext(GlobalContext)\n\n return <StyledWrap $cardSize={size} {...props} />\n}\n\nexport default Wrap\n","import React, { useContext } from 'react'\nimport { styled } from 'styled-components'\n\nimport Wrap from './Wrap'\nimport { GlobalContext } from '../../../context/GlobalState'\nimport { classNames, imageProxy } from '../../../utils'\n\nconst ImageWrap = styled(Wrap).attrs({\n className: `${classNames.media} ${classNames.image}`\n})`\n background-image: ${({ url }) => (url ? `url('${imageProxy(url)}')` : '')};\n`\n\nconst ImageComponent = props => {\n const {\n state: { imageUrl }\n } = useContext(GlobalContext)\n\n return <ImageWrap url={imageUrl} {...props} />\n}\n\nexport default ImageComponent\n","/* eslint-disable multiline-ternary */\n\nimport React, { useContext } from 'react'\nimport { styled } from 'styled-components'\n\nimport { emptyStateAnimation, emptyStateImageAnimation } from './CardAnimation'\nimport CardImage from './CardMedia/Image'\nimport { Content } from './CardContent'\nimport { GlobalContext } from '../../context/GlobalState'\nimport { isLarge, isSmall } from '../../utils'\n\nconst MediaEmpty = styled(CardImage)`\n ${emptyStateImageAnimation};\n`\n\nconst HeaderEmpty = styled('span')`\n opacity: 0.8;\n height: 16px;\n width: ${({ $cardSize }) => (!isSmall($cardSize) ? '60%' : '75%')};\n display: block;\n background: #e1e8ed;\n margin: ${({ $cardSize }) =>\n !isSmall($cardSize) ? '2px 0 8px' : '0 20px 0 0'};\n ${emptyStateAnimation};\n\n ${({ $cardSize }) =>\n !isLarge($cardSize) &&\n `\n height: 15px;\n `};\n`\n\nconst DescriptionEmpty = styled('span')`\n opacity: 0.8;\n height: 14px;\n width: 95%;\n display: block;\n position: relative;\n ${emptyStateAnimation};\n animation-delay: 0.125s;\n`\n\nconst FooterEmpty = styled('span')`\n opacity: 0.8;\n height: 12px;\n width: 30%;\n display: block;\n ${emptyStateAnimation} animation-delay: .25s;\n\n ${({ $cardSize }) =>\n !isLarge($cardSize) &&\n `\n height: 10px;\n `};\n`\n\nconst CardEmptyState = () => {\n const {\n props: { size }\n } = useContext(GlobalContext)\n const isSmallCard = isSmall(size)\n\n return (\n <>\n <MediaEmpty $cardSize={size} />\n <Content $cardSize={size}>\n <HeaderEmpty $cardSize={size} />\n {!isSmallCard ? (\n <>\n <DescriptionEmpty $cardSize={size} />\n <DescriptionEmpty\n $cardSize={size}\n style={{ marginBottom: '12px' }}\n />\n </>\n ) : null}\n <FooterEmpty />\n </Content>\n </>\n )\n}\n\nexport default CardEmptyState\n","import { styled } from 'styled-components'\nimport { transition } from '../../../../theme'\n\nconst MediaButton = styled('div')`\n backface-visibility: hidden;\n filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));\n transition: ${transition.short('transform')};\n will-change: transform;\n\n > svg {\n display: block;\n }\n\n &:active:not(:focus) {\n transform: scale(0.9);\n }\n`\n\nexport default MediaButton\n","import React, { useMemo } from 'react'\nimport { styled } from 'styled-components'\n\nimport MediaButton from './MediaButton'\nimport { classNames, media, isLarge } from '../../../../utils'\nimport { font, transition } from '../../../../theme'\n\nconst VolumeMute = props => (\n <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14' {...props}>\n <path\n fill='#FFF'\n fillRule='evenodd'\n stroke='none'\n strokeWidth='1'\n d='M15.5 6.205l-.705-.705L13 7.295 11.205 5.5l-.705.705L12.295 8 10.5 9.795l.705.705L13 8.705l1.795 1.795.705-.705L13.705 8 15.5 6.205zM9 15a.5.5 0 01-.355-.15L4.835 11H1.5a.5.5 0 01-.5-.5v-5a.5.5 0 01.5-.5h3.335l3.81-3.85a.5.5 0 01.705 0 .5.5 0 01.15.35v13a.5.5 0 01-.5.5z'\n transform='translate(-1 -1)'\n />\n </svg>\n)\n\nconst VolumeUp = props => (\n <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14' {...props}>\n <path\n fill='#FFF'\n fillRule='evenodd'\n stroke='none'\n strokeWidth='1'\n d='M13.58 4.04l-.765.645a5 5 0 01-.145 6.615l.735.7a6 6 0 00.175-7.94v-.02zM10.79 6a3 3 0 01-.09 3.97l.735.68a4 4 0 00.115-5.295L10.79 6zM9 15a.5.5 0 01-.355-.15L4.835 11H1.5a.5.5 0 01-.5-.5v-5a.5.5 0 01.5-.5h3.335l3.81-3.85a.5.5 0 01.705 0 .5.5 0 01.15.35v13a.5.5 0 01-.5.5z'\n transform='translate(-1 -1)'\n />\n </svg>\n)\n\nconst BottomControls = styled('div')`\n z-index: 2;\n position: absolute;\n bottom: ${({ $cardSize }) => (isLarge($cardSize) ? 18 : 14)}px;\n left: 0;\n right: 0;\n display: flex;\n justify-content: center;\n align-items: center;\n transition: ${transition.medium('opacity')};\n will-change: opacity;\n`\n\nconst VolumeIcon = styled('svg')`\n stroke: #fff;\n`\n\nconst VolumeButton = styled(MediaButton).attrs({\n className: classNames.volumeControl\n})`\n ${VolumeIcon} {\n width: ${({ $cardSize }) => (isLarge($cardSize) ? 16 : 14)}px;\n height: ${({ $cardSize }) => (isLarge($cardSize) ? 16 : 14)}px;\n\n ${({ $cardSize }) =>\n !isLarge($cardSize) &&\n media.mobile`\n width: 12px;\n height: 12px;\n `}\n }\n`\n\nconst PlaybackRateButton = styled(MediaButton).attrs({\n className: classNames.rateControl\n})`\n font-size: ${({ $cardSize }) => (isLarge($cardSize) ? 12 : 10)}px;\n min-width: ${({ $cardSize }) => (isLarge($cardSize) ? 33 : 28)}px;\n line-height: 1;\n font-weight: bold;\n border: 1.5px solid #fff;\n border-radius: 9999px;\n padding: 1px 5px;\n text-align: center;\n color: #fff;\n margin-left: 10px;\n\n ${({ $cardSize }) =>\n !isLarge($cardSize) &&\n media.mobile`\n font-size: 8px;\n margin-left: 8px;\n min-width: 23px;\n `}\n`\n\nconst TimeLabel = styled('span').attrs({ className: classNames.progressTime })`\n margin: ${({ $right }) => (!$right ? '0 auto 0 0' : '0 0 0 auto')};\n font-family: ${font.mono};\n font-size: 12px;\n padding: 0 16px;\n color: #fff;\n text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);\n`\n\nconst FooterControls = ({\n $cardSize,\n currentTime,\n endTime,\n isMuted,\n onMuteClick,\n onPlaybackRateClick,\n playbackRate\n}) => {\n const VolumeComponent = useMemo(\n () => (isMuted ? VolumeMute : VolumeUp),\n [isMuted]\n )\n const isLargeCard = useMemo(() => isLarge($cardSize), [$cardSize])\n\n return (\n <BottomControls $cardSize={$cardSize}>\n {isLargeCard && <TimeLabel>{currentTime}</TimeLabel>}\n\n <VolumeButton\n title={isMuted ? 'Unmute' : 'Mute'}\n $cardSize={$cardSize}\n onClick={onMuteClick}\n >\n <VolumeIcon as={VolumeComponent} />\n </VolumeButton>\n\n <PlaybackRateButton\n title='Playback Rate'\n $cardSize={$cardSize}\n onClick={onPlaybackRateClick}\n >\n <span>{playbackRate}x</span>\n </PlaybackRateButton>\n\n {isLargeCard && <TimeLabel $right>{endTime}</TimeLabel>}\n </BottomControls>\n )\n}\n\nexport default FooterControls\n","import React, { useMemo } from 'react'\nimport { styled, css } from 'styled-components'\n\nimport MediaButton from './MediaButton'\nimport { classNames, isSmall, isLarge, media } from '../../../../utils'\n\nconst Pause = props => (\n <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 20' {...props}>\n <path\n fill='#FFF'\n fillRule='evenodd'\n stroke='none'\n strokeWidth='1'\n d='M12 6h-2a2 2 0 00-2 2v16a2 2 0 002 2h2a2 2 0 002-2V8a2 2 0 00-2-2zm10 0h-2a2 2 0 00-2 2v16a2 2 0 002 2h2a2 2 0 002-2V8a2 2 0 00-2-2z'\n transform='translate(-8 -6)'\n />\n </svg>\n)\n\nconst Play = props => (\n <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 21 24' {...props}>\n <path\n fill='#FFF'\n fillRule='evenodd'\n stroke='none'\n strokeWidth='1'\n d='M7 28a1 1 0 01-1-1V5a1 1 0 011.501-.865l19 11a1 1 0 010 1.73l-19 11A.998.998 0 017 28z'\n transform='translate(-6 -4)'\n />\n </svg>\n)\n\nconst iconSizes = {\n large: '50px',\n normal: '35px',\n small: '20px'\n}\n\nconst PlaybackIcon = styled('svg')`\n stroke: #fff;\n`\n\nconst PlaybackButtonWrap = styled(MediaButton).attrs({\n className: classNames.playbackControl\n})`\n ${PlaybackIcon} {\n ${({ $cardSize }) => css`\n width: ${iconSizes[$cardSize]};\n height: ${iconSizes[$cardSize]};\n padding: ${isLarge($cardSize) ? 0 : '8px'};\n\n ${!isLarge($cardSize) &&\n !isSmall($cardSize) &&\n media.mobile`\n width: calc(${iconSizes.small} * 1.2);\n height: calc(${iconSizes.small} * 1.2);\n `}\n `}\n }\n`\n\nconst PlaybackButton = ({ $isPlaying, ...props }) => {\n const PlaybackComponent = useMemo(\n () => ($isPlaying ? Pause : Play),\n [$isPlaying]\n )\n\n return (\n <PlaybackButtonWrap title={$isPlaying ? 'Pause' : 'Play'} {...props}>\n <PlaybackIcon as={PlaybackComponent} />\n </PlaybackButtonWrap>\n )\n}\n\nexport default PlaybackButton\n","import { styled, css } from 'styled-components'\n\nimport { transition } from '../../../../../theme'\n\nconst SCRUBBER_SIZE = 12\n\nconst scrubberSizeScales = { normal: 0.8, small: 0.9 }\n\nconst getScrubberSize = size =>\n Math.floor(SCRUBBER_SIZE * (scrubberSizeScales[size] || 1))\n\nconst Scrubber = styled('div').attrs(({ $isVisible, $positionX }) => ({\n style: {\n left: $positionX,\n transform: `scale(${$isVisible ? 1 : 0.5}) translate(-50%, -50%)`,\n opacity: $isVisible ? 1 : 0,\n visibility: $isVisible ? '$visible' : 'hidden'\n }\n}))`\n position: absolute;\n top: 50%;\n background: #ffffff;\n border-radius: 50%;\n transform-origin: center center;\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);\n transition: ${transition.short('transform', 'opacity', 'visibility')};\n will-change: left, transform, opacity, visibility;\n backface-visibility: hidden;\n z-index: 3;\n\n ${({ $cardSize }) => {\n const scrubberSize = getScrubberSize($cardSize)\n return css`\n height: ${scrubberSize}px;\n width: ${scrubberSize}px;\n `\n }}\n`\n\nexport default Scrubber\n","import React, { forwardRef } from 'react'\nimport { styled } from 'styled-components'\n\nimport { font, transition } from '../../../../../theme'\n\nconst BASE_FONT_SIZE = 11\n\nconst sizeScales = { normal: 0.8 }\nconst getMarkerFontSize = size => BASE_FONT_SIZE * (sizeScales[size] || 1)\n\nconst TooltipBase = styled('span').attrs(\n ({ $position, $isDragging, $visible }) => ({\n style: {\n left: `${$position}px`,\n top: $visible ? '-4px' : '0px',\n visibility: $visible ? '$visible' : 'hidden',\n opacity: $visible ? 1 : 0,\n transform: `translate(-50%, ${!$isDragging ? -100 : -110}%)`\n }\n })\n)`\n position: absolute;\n background: rgba(24, 25, 25, 0.75);\n color: #fff;\n text-shadow: 0 1px 2px rgba(24, 25, 25, 0.15);\n padding: 2px 3px;\n border-radius: 4px;\n font-family: ${font.mono};\n font-size: ${({ $cardSize }) => getMarkerFontSize($cardSize)}px;\n line-height: 1;\n transition: ${transition.medium('opacity', 'visibility', 'transform')},\n ${transition.long('top')};\n will-change: top, left, visibility, opacity, transform;\n backface-visibility: hidden;\n`\n\nconst Tooltip = forwardRef(\n ({ $isDragging, $isVisible, label, $positionX, size, ...props }, ref) => (\n <>\n <TooltipBase\n $visible={$isVisible}\n $position={$positionX}\n $cardSize={size}\n ref={ref}\n $isDragging={$isDragging}\n {...props}\n >\n {label}\n </TooltipBase>\n </>\n )\n)\n\nTooltip.displayName = 'Tooltip'\n\nexport default Tooltip\n","import React, { useCallback, useContext, useMemo, useRef } from 'react'\nimport { styled, css } from 'styled-components'\n\nimport { transition } from '../../../../../theme'\nimport {\n clampNumber,\n classNames,\n formatSeconds,\n isSmall\n} from '../../../../../utils'\nimport { GlobalContext } from '../../../../../context/GlobalState'\n\nimport Scrubber from './Scrubber'\nimport Tooltip from './Tooltip'\n\nconst HEIGHT = 6\nconst PADDING = 6\n\nconst heightScales = { normal: 0.7, small: 0.6 }\nconst activeHeightScales = { small: 0.9, large: 1.4 }\n\nconst getProgressBarHeight = size =>\n Math.floor(HEIGHT * (heightScales[size] || 1))\n\nconst getProgressBarActiveHeight = size =>\n Math.floor(HEIGHT * (activeHeightScales[size] || 1))\n\nconst OuterWrap = styled('div').attrs(() => ({\n className: classNames.progressBar\n}))`\n position: relative;\n padding: ${PADDING}px ${PADDING / 2}px ${PADDING / 2}px;\n z-index: 2;\n backface-visibility: hidden;\n`\n\nconst BarsWrap = styled('div').attrs(({ $cardSize, $isDragging }) => {\n if ($isDragging) {\n const activeHeight = getProgressBarActiveHeight($cardSize)\n\n return {\n style: {\n height: `${activeHeight}px`\n }\n }\n }\n\n return {}\n})`\n background: transparent;\n border-radius: 9999px;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);\n background: rgba(255, 255, 255, 0.15);\n transition: ${transition.short('height')};\n will-change: height;\n pointer-events: none;\n position: relative;\n\n ${({ $cardSize }) => {\n const height = getProgressBarHeight($cardSize)\n const activeHeight = getProgressBarActiveHeight($cardSize)\n\n return css`\n height: ${height}px;\n\n ${OuterWrap}:hover & {\n height: ${activeHeight}px;\n }\n `\n }}\n`\n\nconst ProgressLine = styled('div')`\n border-radius: inherit;\n height: 100%;\n position: relative;\n overflow: hidden;\n`\n\nconst ProgressMask = styled('div').attrs(({ $maskScale }) => ({\n style: {\n transform: `scaleX(${$maskScale})`\n }\n}))`\n position: absolute;\n left: 0;\n top: -50%;\n height: 200%;\n width: 100%;\n background: #ffffff;\n transform-origin: left center;\n will-change: transform;\n`\n\nconst ProgressHover = styled('div').attrs(\n ({ $cursorRatio, $isHovering, $progressPercent }) => ({\n style: {\n left: $progressPercent,\n transform: `scaleX(${$cursorRatio})`,\n opacity: $isHovering ? 1 : 0,\n visibility: $isHovering ? '$visible' : 'hidden'\n }\n })\n)`\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n background: rgba(255, 255, 255, 0.4);\n transform-origin: left center;\n transition: ${transition.short('opacity', 'visibility')};\n will-change: left, transform, opacity, $visible;\n`\n\nconst BufferedChunk = styled('div').attrs(({ start, end }) => ({\n style: {\n left: `${start}px`,\n right: `${end}px`\n }\n}))`\n background: rgba(255, 255, 255, 0.35);\n position: absolute;\n top: 0;\n bottom: 0;\n`\n\nconst ProgressBar = ({\n bufferedMedia,\n cursorX,\n duration,\n hoveredTime,\n $isDragging,\n $isHovering,\n onClick,\n onMouseDown,\n onMouseOver,\n progress,\n showTooltip\n}) => {\n const {\n props: { size }\n } = useContext(GlobalContext)\n const wrapRef = useRef()\n const tooltipRef = useRef()\n\n const isSmallCard = useMemo(() => isSmall(size), [size])\n\n const getWrapWidth = useCallback(() => {\n if (wrapRef.current) {\n return wrapRef.current.getBoundingClientRect().width - PADDING\n }\n\n return 0\n }, [])\n\n const progressRatio = useMemo(\n () => clampNumber(progress / duration, 0, 1),\n [duration, progress]\n )\n\n const $progressPercent = useMemo(\n () => `${clampNumber(progressRatio * 100, 1, 99)}%`,\n [progressRatio]\n )\n\n const $cursorRatio = useMemo(() => {\n if (wrapRef.current) {\n const wrapWidth = getWrapWidth()\n const startPoint = progressRatio * wrapWidth\n const cursorPosition = cursorX - startPoint\n const width = wrapWidth - startPoint\n\n if (cursorPosition > 0) {\n return clampNumber((cursorPosition / width).toFixed(3), 0, 0.99)\n }\n }\n\n return 0\n }, [cursorX, getWrapWidth, progressRatio])\n\n const bufferedMediaChunks = useMemo(() => {\n const wrapWidth = getWrapWidth()\n\n return bufferedMedia.map((chunk, key) => {\n const start = chunk.start * wrapWidth\n const end = wrapWidth - chunk.end * wrapWidth\n\n return { key, start, end }\n })\n }, [bufferedMedia, getWrapWidth])\n\n const tooltipLabel = useMemo(() => formatSeconds(hoveredTime), [hoveredTime])\n\n const tooltipPositionX = useMemo(() => {\n if (wrapRef.current && tooltipRef.current) {\n const wrapWidth = getWrapWidth()\n const tooltipWidth = tooltipRef.current.getBoundingClientRect().width\n const tooltipHalf = tooltipWidth / 2\n\n return clampNumber(cursorX, tooltipHalf, wrapWidth - tooltipHalf)\n }\n\n return 0\n }, [cursorX, getWrapWidth])\n\n const mouseEvents = useMemo(\n () => ({\n onClick,\n onMouseDown,\n onMouseOver\n }),\n [onClick, onMouseDown, onMouseOver]\n )\n\n const showAccessories = useMemo(\n () => $isDragging || $isHovering,\n [$isDragging, $isHovering]\n )\n\n return (\n <OuterWrap $cardSize={size} ref={wrapRef} {...mouseEvents}>\n <BarsWrap $cardSize={size} $isDragging={$isDragging}>\n <ProgressLine>\n <ProgressHover\n $cursorRatio={$cursorRatio}\n $isHovering={$isHovering}\n $progressPercent={$progressPercent}\n />\n\n {bufferedMediaChunks.map(({ key, ...chunk }) => (\n <BufferedChunk key={key} {...chunk} />\n ))}\n\n <ProgressMask $maskScale={progressRatio} />\n </ProgressLine>\n\n <Scrubber\n $cardSize={size}\n $isVisible={showAccessories}\n $positionX={$progressPercent}\n />\n\n {!isSmallCard && (\n <Tooltip\n $isDragging={$isDragging}\n $isVisible={showAccessories}\n label={tooltipLabel}\n $positionX={tooltipPositionX}\n ref={tooltipRef}\n size={size}\n />\n )}\n </BarsWrap>\n </OuterWrap>\n )\n}\n\nexport default ProgressBar\n","import React, { useMemo } from 'react'\nimport { styled } from 'styled-components'\n\nimport MediaButton from './MediaButton'\nimport { media, isLarge } from '../../../../utils'\n\nconst Backward = ({ $cardSize, ...props }) => (\n <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 29' {...props}>\n <path\n fill='#FFF'\n fillRule='evenodd'\n stroke='none'\n strokeWidth='1'\n d='M4 18c0 6.627 5.373 12 12 12s12-5.373 12-12S22.627 6 16 6h-4V1L6 7l6 6V8h4c5.523 0 10 4.477 10 10s-4.477 10-10 10S6 23.523 6 18H4zm15.63 4.13a2.84 2.84 0 01-1.28-.27 2.44 2.44 0 01-.89-.77 3.57 3.57 0 01-.52-1.25 7.69 7.69 0 01-.17-1.68 7.83 7.83 0 01.17-1.68c.094-.445.27-.87.52-1.25.23-.325.535-.59.89-.77.4-.188.838-.28 1.28-.27a2.44 2.44 0 012.16 1 5.23 5.23 0 01.7 2.93 5.23 5.23 0 01-.7 2.93 2.44 2.44 0 01-2.16 1.08zm0-1.22c.411.025.8-.19 1-.55a3.38 3.38 0 00.37-1.51v-1.38a3.31 3.31 0 00-.29-1.5 1.23 1.23 0 00-2.06 0 3.31 3.31 0 00-.29 1.5v1.38a3.38 3.38 0 00.29 1.51c.195.356.575.57.98.55zm-9 1.09v-1.18h2v-5.19l-1.86 1-.55-1.06 2.32-1.3H14v6.5h1.78V22h-5.15z'\n transform='translate(-4 -1)'\n />\n </svg>\n)\n\nconst Forward = ({ $cardSize, ...props }) => (\n <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 29' {...props}>\n <path\n fill='#FFF'\n fillRule='evenodd'\n stroke='none'\n strokeWidth='1'\n d='M26 18c0 5.523-4.477 10-10 10S6 23.523 6 18 10.477 8 16 8h4v5l6-6-6-6v5h-4C9.373 6 4 11.373 4 18s5.373 12 12 12 12-5.373 12-12h-2zm-6.36 4.13a2.81 2.81 0 01-1.28-.27 2.36 2.36 0 01-.89-.77 3.39 3.39 0 01-.47-1.25 7.12 7.12 0 01-.17-1.68 7.24 7.24 0 01.17-1.68 3.46 3.46 0 01.52-1.25 2.36 2.36 0 01.89-.77c.4-.19.838-.282 1.28-.27a2.44 2.44 0 012.16 1 5.31 5.31 0 01.7 2.93 5.31 5.31 0 01-.7 2.93 2.44 2.44 0 01-2.21 1.08zm0-1.22a1 1 0 001-.55c.22-.472.323-.99.3-1.51v-1.38a3.17 3.17 0 00-.3-1.5 1.22 1.22 0 00-2.05 0 3.18 3.18 0 00-.29 1.5v1.38a3.25 3.25 0 00.29 1.51 1 1 0 001.05.55zm-7.02-3.49c.355.035.71-.06 1-.27a.84.84 0 00.31-.68v-.08a.94.94 0 00-.3-.74 1.2 1.2 0 00-.83-.27 1.65 1.65 0 00-.89.24 2.1 2.1 0 00-.68.68l-.93-.83a5.37 5.37 0 01.44-.51 2.7 2.7 0 01.54-.4 2.55 2.55 0 01.7-.27 3.25 3.25 0 01.87-.1 3.94 3.94 0 011.06.14c.297.078.576.214.82.4.224.168.408.383.54.63.123.26.184.543.18.83a2 2 0 01-.11.67 1.82 1.82 0 01-.32.52 1.79 1.79 0 01-.47.36 2.27 2.27 0 01-.57.2V18c.219.04.431.11.63.21a1.7 1.7 0 01.85.93c.084.234.124.481.12.73a2 2 0 01-.2.92 2 2 0 01-.58.72 2.66 2.66 0 01-.89.45 3.76 3.76 0 01-1.15.16 4.1 4.1 0 01-1-.11 3.1 3.1 0 01-.76-.31 2.76 2.76 0 01-.56-.45 4.22 4.22 0 01-.44-.55l1.07-.81c.082.147.175.288.28.42.105.128.226.243.36.34.137.097.29.171.45.22a2 2 0 00.57.07 1.45 1.45 0 001-.3 1.12 1.12 0 00.34-.85v-.08a1 1 0 00-.37-.8 1.78 1.78 0 00-1.06-.28h-.76v-1.21h.74z'\n transform='translate(-4 -1)'\n />\n </svg>\n)\n\nconst SeekIcon = styled('svg')`\n stroke: #fff;\n width: ${({ $cardSize }) => (isLarge($cardSize) ? 30 : 24)}px;\n height: ${({ $cardSize }) => (isLarge($cardSize) ? 30 : 24)}px;\n\n ${({ $cardSize }) =>\n !isLarge($cardSize) &&\n media.mobile`\n width: 0;\n height: 0;\n `}\n`\n\nconst SeekButtonWrap = styled(MediaButton)`\n margin: 0 ${({ $cardSize }) => (isLarge($cardSize) ? '28px' : '3px')};\n`\n\nconst SeekButton = ({ type = 'rewind', $cardSize, ...props }) => {\n const IconComponent = useMemo(\n () => (type === 'rewind' ? Backward : Forward),\n [type]\n )\n\n return (\n <SeekButtonWrap\n title={type === 'rewind' ? 'Rewind' : 'Forward'}\n $cardSize={$cardSize}\n {...props}\n >\n <SeekIcon as={IconComponent} $cardSize={$cardSize} />\n </SeekButtonWrap>\n )\n}\n\nexport default SeekButton\n","import React from 'react'\nimport { styled, css, keyframes } from 'styled-components'\n\nimport { transition } from '../../../../theme'\nimport { classNames } from '../../../../utils'\n\nimport MediaButton from './MediaButton'\n\nconst BASE_SIZE = 12\nconst BASE_OFFSET = 6\n\nconst offsetScales = { normal: 0.8, small: 0.6 }\nconst sizeScales = { normal: 0.9, small: 0.8 }\n\nconst getSpinnerOffset = size =>\n Math.floor(BASE_OFFSET * (offsetScales[size] || 1))\n\nconst getSpinnerSize = size => Math.floor(BASE_SIZE * (sizeScales[size] || 1))\n\nconst rotate = keyframes`\n 100% {\n transform: rotate(360deg);\n }\n`\n\nconst dash = keyframes`\n 0% {\n stroke-dasharray: 1, 150;\n stroke-dashoffset: 0;\n }\n 50% {\n stroke-dasharray: 90, 150;\n stroke-dashoffset: -35;\n }\n 100% {\n stroke-dasharray: 90, 150;\n stroke-dashoffset: -124;\n }\n`\n\nconst Wrap = styled(MediaButton).attrs(({ $isVisible }) => ({\n style: {\n opacity: $isVisible ? 1 : 0,\n visibility: $isVisible ? '$visible' : 'hidden'\n }\n}))(({ $cardSize }) => {\n const size = `${getSpinnerSize($cardSize)}px`\n const offset = `${getSpinnerOffset($cardSize)}px`\n\n return css`\n position: absolute;\n width: ${size};\n right: ${offset};\n top: ${offset};\n transition: ${transition.medium('opacity', 'visibility')};\n will-change: opacity, visibility;\n pointer-events: none;\n `\n})\n\nconst Svg = styled('svg')`\n width: 100%;\n animation: ${rotate} 2s linear infinite;\n will-change: transform;\n`\n\nconst Circle = styled('circle')`\n stroke: #fff;\n stroke-linecap: round;\n stroke-width: 7;\n fill: none;\n animation: ${dash} 1.5s ease-in-out infinite;\n will-change: stroke-dasharray, stroke-dashoffset;\n`\n\nconst Spinner = ({ size, $isVisible }) => (\n <Wrap $cardSize={size} className={classNames.spinner} $isVisible={$isVisible}>\n <Svg viewBox='0 0 50 50'>\n <Circle cx='25' cy='25' r='20' />\n </Svg>\n </Wrap>\n)\n\nexport default Spinner\n","/* eslint-disable multiline-ternary */\n\nimport React, {\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState\n} from 'react'\n\nimport { styled, css } from 'styled-components'\n\nimport FooterControls from './FooterControls'\nimport PlaybackButton from './PlaybackButton'\nimport ProgressBar from './ProgressBar'\nimport SeekButton from './SeekButton'\nimport Spinner from './Spinner'\nimport { transition } from '../../../../theme'\n\nimport {\n classNames,\n formatSeconds,\n isSmall,\n isFunction,\n clampNumber\n} from '../../../../utils'\n\nimport { GlobalContext } from '../../../../context/GlobalState'\n\nconst SPACE_KEY = 32\nconst L_ARROW_KEY = 37\nconst R_ARROW_KEY = 39\nconst M_KEY = 77\n\nconst OuterWrap = styled('div').attrs({ className: classNames.mediaControls })`\n position: absolute;\n left: 0;\n top: 0;\n right: 0;\n bottom: 0;\n transition: ${transition.long('background')}, ${transition.medium('opacity')};\n will-change: background;\n display: flex;\n flex-direction: column;\n pointer-events: auto;\n\n ${({ $hasInteracted, $isDragging, $isPlaying }) => {\n const bg = 'rgba(0, 0, 0, 0.35)'\n const dragBg = 'rgba(0, 0, 0, 0.2)'\n const isPaused = $hasInteracted && !$isPlaying\n\n return css`\n .${classNames.main}:hover & {\n background: ${!$isDragging ? bg : dragBg};\n }\n\n .${classNames.main}:not(:hover) & {\n opacity: ${!$hasInteracted || isPaused ? 1 : 0};\n ${isPaused && `background: ${bg}`};\n }\n `\n }}\n`\n\nconst InnerWrap = styled('div')`\n position: absolute;\n left: 0;\n top: 0;\n right: 0;\n bottom: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 2;\n`\n\nconst ControlsTop = styled('div')`\n flex: 1;\n\n ${({ $isVisible }) =>\n !$isVisible &&\n css`\n *[class*='${classNames.mediaControls}']:not(.${classNames.progressTime}) {\n transition: ${transition.medium('opacity', 'visibility')};\n opacity: 0;\n visibility: hidden;\n }\n `}\n`\n\nconst getNextPlaybackRate = rate => {\n switch (rate) {\n case 1:\n return 1.25\n case 1.25:\n return 1.5\n case 1.5:\n return 2\n default:\n return 1\n }\n}\n\nconst Controls = ({ MediaComponent, mediaProps }) => {\n const {\n props: { autoPlay, controls, mediaRef: propRef, muted, loop, size }\n } = useContext(GlobalContext)\n const [duration, setDuration] = useState(0)\n const [progress, setProgress] = useState(0)\n const [buffered, setBuffered] = useState([])\n const [cursorX, setCursorX] = useState(0)\n const [hoveredTime, setHoveredTime] = useState(0)\n const [$isPlaying, setIsPlaying] = useState(autoPlay)\n const [isMuted, setIsMuted] = useState(muted)\n const [isBuffering, setIsBuffering] = useState(false)\n const [$isHovering, setIsHovering] = useState(false)\n const [$isDragging, setIsDragging] = useState(false)\n const [playbackRate, setPlaybackRate] = useState(1)\n const [$hasInteracted, setHasInteracted] = useState(autoPlay)\n const [pausedByDrag, setPausedByDrag] = useState(false)\n\n const mediaRef = useRef()\n const setRefs = useCallback(\n node => {\n mediaRef.current = node\n\n if (propRef) {\n if (isFunction(propRef)) {\n propRef(node)\n } else {\n propRef.current = node\n }\n }\n },\n [propRef]\n )\n\n const isNotSmall = useMemo(() => !isSmall(size), [size])\n\n const mediaEvents = useMemo(\n () => ({\n onCanPlay: () => setIsBuffering(false),\n onLoadedMetadata: e => setDuration(e.currentTarget.duration),\n onPause: () => setIsPlaying(false),\n onPlay: () => setIsPlaying(true),\n onPlaying: () => setIsBuffering(false),\n onProgress: e => setBuffered(e.currentTarget.buffered),\n onRateChange: e => setPlaybackRate(e.currentTarget.playbackRate),\n onTimeUpdate: e => setProgress(e.currentTarget.currentTime),\n onVolumeChange: e => setIsMuted(e.currentTarget.muted),\n onWaiting: e => setIsBuffering(true)\n }),\n []\n )\n\n const evaluateCursorPosition = useCallback(event => {\n if (mediaRef.current) {\n const bounds = event.currentTarget.getBoundingClientRect()\n const cursor = clampNumber(\n Math.floor(event.clientX - bounds.left),\n 0,\n bounds.width\n )\n const time = (cursor / bounds.width) * mediaRef.current.duration\n\n return { cursor, time }\n }\n\n return { cursor: 0, time: 0 }\n }, [])\n\n const togglePlayback = useCallback(() => {\n if (mediaRef.current) {\n if (mediaRef.current.paused) {\n if (!$hasInteracted) {\n setHasInteracted(true)\n }\n\n mediaRef.current.play()\n } else {\n mediaRef.current.pause()\n }\n }\n }, [$hasInteracted])\n\n const jumpTo = useCallback(time => {\n if (mediaRef.current) {\n const t = clampNumber(time, 0, mediaRef.current.duration)\n\n mediaRef.current.currentTime = t\n setProgress(t)\n }\n }, [])\n\n const onSeekClick = useCallback(\n (event, type) => {\n event.preventDefault()\n event.stopPropagation()\n\n if (mediaRef.current) {\n const { currentTime } = mediaRef.current\n\n jumpTo(type === 'rewind' ? currentTime - 10 : currentTime + 30)\n }\n },\n [jumpTo]\n )\n\n const onMuteClick = useCallback(event => {\n event.preventDefault()\n event.stopPropagation()\n\n if (mediaRef.current) {\n mediaRef.current.muted = !mediaRef.current.muted\n }\n }, [])\n\n const onPlaybackRateClick = useCallback(event => {\n event.preventDefault()\n event.stopPropagation()\n\n if (mediaRef.current) {\n mediaRef.current.playbackRate = getNextPlaybackRate(\n mediaRef.current.playbackRate\n )\n }\n }, [])\n\n const onProgressBarClick = useCallback(event => {\n event.preventDefault()\n event.stopPropagation()\n setIsDragging(false)\n }, [])\n\n const onProgressBarMouseDown = useCallback(\n event => {\n event.preventDefault()\n event.stopPropagation()\n setIsDragging(true)\n\n const { time } = evaluateCursorPosition(event)\n jumpTo(time)\n },\n [evaluateCursorPosition, jumpTo]\n )\n\n const onProgressBarMouseOver = useCallback(() => setIsHovering(true), [])\n\n const onWrapClick = useCallback(\n event => {\n event.preventDefault()\n event.stopPropagation()\n\n if ($isDragging) {\n setIsDragging(false)\n } else {\n togglePlayback()\n }\n },\n [$isDragging, togglePlayback]\n )\n\n const onWrapMouseMove = useCallback(\n event => {\n if (($isDragging || $isHovering) && mediaRef.current) {\n event.preventDefault()\n const { cursor, time } = evaluateCursorPosition(event)\n\n setHoveredTime(time)\n setCursorX(cursor)\n\n if ($isDragging) {\n if (!mediaRef.current.paused) {\n mediaRef.current.pause()\n setPausedByDrag(true)\n }\n\n jumpTo(time)\n }\n }\n },\n [evaluateCursorPosition, $isDragging, $isHovering, jumpTo]\n )\n\n const onWrapMouseOver = useCallback(\n event => {\n if ($isDragging && event.buttons === 0) {\n setIsDragging(false)\n }\n },\n [$isDragging]\n )\n\n const onWrapKeyDown = useCallback(\n event => {\n if ($isDragging) {\n return\n }\n\n const { keyCode } = event\n\n if (\n [SPACE_KEY, L_ARROW_KEY, R_ARROW_KEY, M_KEY].includes(keyCode) &&\n mediaRef.current\n ) {\n event.preventDefault()\n\n switch (keyCode) {\n case SPACE_KEY:\n togglePlayback()\n break\n case L_ARROW_KEY:\n jumpTo(mediaRef.current.currentTime - 5)\n break\n case R_ARROW_KEY:\n jumpTo(mediaRef.current.currentTime + 5)\n break\n case M_KEY:\n mediaRef.current.muted = !mediaRef.current.muted\n break\n }\n }\n },\n [$isDragging, jumpTo, togglePlayback]\n )\n\n const outerWrapEvents = useMemo(\n () => ({\n onClick: onWrapClick,\n onKeyDown: onWrapKeyDown,\n onMouseMove: onWrapMouseMove,\n onMouseOut: () => setIsHovering(false),\n onMouseOver: onWrapMouseOver\n }),\n [onWrapClick, onWrapKeyDown, onWrapMouseMove, onWrapMouseOver]\n )\n\n const outerWrapTitle = useMemo(\n () => ($hasInteracted ? { title: '' } : {}),\n [$hasInteracted]\n )\n\n const bufferedMedia = useMemo(() => {\n if (buffered && buffered.length && mediaRef.current) {\n return [...Array(buffered.length).keys()].map(index => {\n return {\n start: buffered.start(index) / mediaRef.current.duration,\n end: buffered.end(index) / mediaRef.current.duration\n }\n })\n }\n\n return []\n }, [buffered])\n\n const currentTime = useMemo(() => formatSeconds(progress || 0), [progress])\n const endTime = useMemo(() => formatSeconds(duration || 0), [duration])\n\n const footerControlsProps = useMemo(\n () => ({\n $cardSize: size,\n currentTime,\n endTime,\n isMuted,\n onMuteClick,\n onPlaybackRateClick,\n playbackRate\n }),\n [\n currentTime,\n endTime,\n i