/** * @title Progress Circle * @description 작업이 진행 중임을 알리거나 작업 시간을 시각적으로 나타내는 컴포넌트예요. */import * as React from 'react';import { cva, type VariantProps } from 'class-variance-authority';import { cn } from '@/lib/utils';// SEED Rootage spec: progress-circle.yaml// indeterminate: length 1.2s head cubic-bezier(0.35,0,0.65,1) / tail cubic-bezier(0.35,0,0.65,0.6),// rotate 1.2s cubic-bezier(0.35,0.25,0.65,0.75)// determinate: length 300ms cubic-bezier(0,0,0.15,1)const KEYFRAMES = [ '@keyframes kd-progress-circle-rotate{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}', '@keyframes kd-progress-circle-head{0%{stroke-dasharray:0,1000%}75%{stroke-dasharray:var(--circumference),1000%}100%{stroke-dasharray:var(--circumference),1000%}}', '@keyframes kd-progress-circle-tail{0%{stroke-dashoffset:0}33.33%{stroke-dashoffset:0}100%{stroke-dashoffset:calc(var(--circumference) * -1)}}',].join('');const progressCircleVariants = cva( [ 'relative box-border inline-flex shrink-0 size-(--size)', 'data-[progress-state=indeterminate]:animate-[kd-progress-circle-rotate_1.2s_cubic-bezier(0.35,0.25,0.65,0.75)_infinite]', 'motion-reduce:data-[progress-state=indeterminate]:animate-[kd-progress-circle-rotate_2.4s_linear_infinite]', ], { variants: { tone: { neutral: '[--range-color:var(--rb-color-palette-gray-500)] [--track-color:var(--rb-color-palette-gray-200)]', // SEED: track $color.palette.carrot-200 → 브랜드 팔레트 200 단계 (bg-brand-weak-pressed) brand: '[--range-color:var(--rb-color-bg-brand-solid)] [--track-color:var(--rb-color-bg-brand-weak-pressed)]', staticWhite: '[--range-color:var(--rb-color-palette-static-white)] [--track-color:var(--rb-color-palette-static-white-alpha-300)]', /** 상위에서 `--track-color`, `--range-color` 를 지정해 사용해요. */ inherit: '', }, size: { '24': '[--size:var(--rb-dimension-x6)] [--thickness:3px]', '40': '[--size:var(--rb-dimension-x10)] [--thickness:5px]', /** 상위에서 `--size`, `--thickness` 를 지정해 사용해요. */ inherit: '', }, }, defaultVariants: { tone: 'neutral', size: '40', }, },);const circleStyle = { '--radius': 'calc(var(--size) / 2 - var(--thickness) / 2)', cx: 'calc(var(--size) / 2)', cy: 'calc(var(--size) / 2)', r: 'var(--radius)', fill: 'transparent', strokeWidth: 'var(--thickness)',} as React.CSSProperties;export interface ProgressCircleProps extends Omit<React.ComponentProps<'svg'>, 'children'>, Omit<VariantProps<typeof progressCircleVariants>, 'size'> { /** 현재 값. 지정하지 않으면 Indeterminate(계속 회전)로 동작해요. */ value?: number; /** 최솟값. */ minValue?: number; /** 최댓값. */ maxValue?: number; /** 24 는 요소 안에서, 40 은 주로 전체 페이지 로딩에 사용해요. */ size?: 24 | 40 | '24' | '40' | 'inherit';}function ProgressCircle({ className, tone, size, value, minValue = 0, maxValue = 100, style, ...props}: ProgressCircleProps) { const indeterminate = typeof value !== 'number'; const percent = indeterminate ? -1 : ((value - minValue) / (maxValue - minValue)) * 100; const state = value === maxValue ? 'complete' : indeterminate ? 'indeterminate' : 'loading'; return ( <> <style href="kd-progress-circle-keyframes" precedence="kd"> {KEYFRAMES} </style> <svg data-slot="progress-circle" data-progress-state={state} role="progressbar" aria-valuenow={indeterminate ? undefined : value} aria-valuemin={minValue} aria-valuemax={maxValue} aria-valuetext={indeterminate ? '로딩 중' : `${Math.round(percent)}%`} className={cn( progressCircleVariants({ tone, size: size === undefined ? undefined : (String(size) as '24') }), className, )} style={style} {...props} > <circle data-slot="progress-circle-track" data-progress-state={state} className="stroke-(--track-color)" style={circleStyle} /> <circle data-slot="progress-circle-range" data-progress-state={state} className={cn( 'origin-center -rotate-90 stroke-(--range-color) [stroke-linecap:round]', // determinate: 300ms cubic-bezier(0, 0, 0.15, 1) 'transition-[stroke-dashoffset,stroke-dasharray] duration-300 ease-[cubic-bezier(0,0,0.15,1)]', 'data-[progress-state=indeterminate]:animate-[kd-progress-circle-head_1.2s_cubic-bezier(0.35,0,0.65,1)_infinite,kd-progress-circle-tail_1.2s_cubic-bezier(0.35,0,0.65,0.6)_infinite]', )} style={ { ...circleStyle, '--percent': percent, '--circumference': 'calc(2 * 3.14159 * var(--radius))', strokeDashoffset: indeterminate ? undefined : 'calc(var(--circumference) * ((100 - var(--percent)) / 100))', strokeDasharray: indeterminate ? undefined : 'var(--circumference)', opacity: value === 0 ? 0 : undefined, } as React.CSSProperties } /> </svg> </> );}export { ProgressCircle, progressCircleVariants };