diff --git a/src/app/App.tsx b/src/app/App.tsx
index 6f11351..772ee46 100644
--- a/src/app/App.tsx
+++ b/src/app/App.tsx
@@ -1,7 +1,9 @@
import './styles/index.scss'
+import { TimeFrameSlider } from '@/widgets/TimeFrameSlider'
+
const App = () => {
- return
Test
+ return
}
export default App
diff --git a/src/widgets/TimeFrameSlider/index.ts b/src/widgets/TimeFrameSlider/index.ts
new file mode 100644
index 0000000..1484132
--- /dev/null
+++ b/src/widgets/TimeFrameSlider/index.ts
@@ -0,0 +1,6 @@
+/**
+ * Widget: TimeFrameSlider
+ * Public API
+ */
+
+export { TimeFrameSlider } from './ui/TimeFrameSlider'
diff --git a/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.module.scss b/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.module.scss
new file mode 100644
index 0000000..b8c5bdb
--- /dev/null
+++ b/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.module.scss
@@ -0,0 +1,162 @@
+.container {
+ position: relative;
+
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+
+ width: 100%;
+ max-width: 1440px;
+ min-height: 100vh;
+ margin: 0 auto;
+ padding: 0 20px;
+
+ color: var(--color-text);
+ font-family: var(--font-family-main);
+
+ border-right: 1px solid var(--color-border);
+ border-left: 1px solid var(--color-border);
+
+ overflow: hidden;
+
+ @media (width <=768px) {
+ min-height: auto;
+ padding: 20px;
+ }
+}
+
+.title {
+ position: relative;
+ z-index: 2;
+
+ margin-bottom: 40px;
+ padding-left: 60px;
+
+ font-weight: 700;
+ font-size: 56px;
+ line-height: 120%;
+
+ @media (width <=768px) {
+ margin-bottom: 20px;
+ padding-left: 0;
+
+ font-size: 20px;
+ }
+}
+
+.content {
+ position: relative;
+
+ display: grid;
+ grid-template-columns: 1fr;
+
+ height: 600px;
+
+ @media (width <=768px) {
+ display: flex;
+ flex-direction: column;
+
+ height: auto;
+ }
+}
+
+.controls {
+ position: absolute;
+ left: 60px;
+ bottom: 50px;
+ z-index: 10;
+
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+
+ @media (width <=768px) {
+ position: static;
+
+ order: 2;
+
+ margin-top: 20px;
+ padding: 0;
+ }
+}
+
+.pagination {
+ margin-bottom: 10px;
+
+ font-size: 14px;
+}
+
+.buttons {
+ display: flex;
+ gap: 20px;
+}
+
+.rotated {
+ transform: rotate(180deg);
+}
+
+.centerDate {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ z-index: 0;
+
+ display: flex;
+ gap: 40px;
+
+ color: var(--color-text);
+ font-weight: 700;
+ font-size: 200px;
+ line-height: 160px;
+
+ transform: translate(-50%, -50%);
+
+ pointer-events: none;
+
+ @media (width <=768px) {
+ position: static;
+
+ gap: 20px;
+ justify-content: center;
+
+ margin-bottom: 40px;
+
+ font-size: 56px;
+
+ transform: none;
+ }
+
+ span:first-child {
+ color: #5d5fef;
+ }
+
+ span:last-child {
+ color: #ef5da8;
+ }
+}
+
+.periodLabel {
+ display: none;
+
+ @media (width <=768px) {
+ order: 1;
+
+ display: block;
+
+ margin-bottom: 40px;
+ padding-top: 40px;
+
+ color: var(--color-text);
+ font-weight: 700;
+ font-size: 20px;
+ text-align: center;
+
+ border-top: 1px solid var(--color-border);
+ }
+}
+
+.circleContainer {
+ @media (width <=768px) {
+ display: none;
+ }
+}
\ No newline at end of file
diff --git a/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.tsx b/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.tsx
new file mode 100644
index 0000000..9ff0d38
--- /dev/null
+++ b/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/TimeFrameSlider.tsx
@@ -0,0 +1,165 @@
+/**
+ * TimeFrameSlider Component
+ * Главный компонент временной шкалы с круговой диаграммой и каруселью событий
+ */
+
+import gsap from 'gsap'
+import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
+
+import { HISTORICAL_PERIODS } from '@/entities/TimePeriod'
+import ChevronSvg from '@/shared/assets/chevron--left.svg'
+import { Button } from '@/shared/ui/Button'
+
+import { ACTIVE_POSITION_ANGLE } from './constants'
+import styles from './TimeFrameSlider.module.scss'
+import { CircleTimeline } from '../CircleTimeline/CircleTimeline'
+import { EventsCarousel } from '../EventsCarousel/EventsCarousel'
+
+/**
+ * Компонент временной шкалы с интерактивной круговой диаграммой
+ *
+ * Отображает исторические периоды на круговой диаграмме с возможностью
+ * переключения между ними. Для каждого периода показывается карусель событий.
+ * Центральные даты анимируются при смене периода с помощью GSAP.
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ */
+export const TimeFrameSlider = memo(() => {
+ const [activePeriod, setActivePeriod] = useState(0)
+ const [rotation, setRotation] = useState(0)
+ const prevRotation = useRef(0)
+ const startYearRef = useRef(null)
+ const endYearRef = useRef(null)
+ const containerRef = useRef(null)
+
+ // Мемоизированные константы
+ const totalPeriods = useMemo(() => HISTORICAL_PERIODS.length, [])
+ const anglePerPoint = useMemo(() => 360 / totalPeriods, [totalPeriods])
+
+ // Текущий период
+ const currentPeriod = useMemo(
+ () => HISTORICAL_PERIODS[activePeriod],
+ [activePeriod]
+ )
+
+ /**
+ * Расчет поворота при изменении активного периода
+ * Использует кратчайший путь для анимации
+ */
+ useEffect(() => {
+ const targetRotation = ACTIVE_POSITION_ANGLE - activePeriod * anglePerPoint
+ const current = prevRotation.current
+ const adjustedTarget =
+ targetRotation - 360 * Math.round((targetRotation - current) / 360)
+
+ setRotation(adjustedTarget)
+ prevRotation.current = adjustedTarget
+ }, [activePeriod, anglePerPoint])
+
+ /**
+ * Анимация центральных дат с использованием GSAP
+ * Плавно изменяет числа при смене периода
+ */
+ useEffect(() => {
+ if (!containerRef.current) return
+
+ const ctx = gsap.context(() => {
+ if (startYearRef.current) {
+ gsap.to(startYearRef.current, {
+ innerText: currentPeriod.yearFrom,
+ snap: { innerText: 1 },
+ duration: 1,
+ ease: 'power2.inOut',
+ })
+ }
+
+ if (endYearRef.current) {
+ gsap.to(endYearRef.current, {
+ innerText: currentPeriod.yearTo,
+ snap: { innerText: 1 },
+ duration: 1,
+ ease: 'power2.inOut',
+ })
+ }
+ }, containerRef)
+
+ return () => ctx.revert()
+ }, [currentPeriod.yearFrom, currentPeriod.yearTo])
+
+ /**
+ * Переключение на предыдущий период
+ * Использует циклическую навигацию
+ */
+ const handlePrev = useCallback(() => {
+ setActivePeriod((prev) => (prev - 1 + totalPeriods) % totalPeriods)
+ }, [totalPeriods])
+
+ /**
+ * Переключение на следующий период
+ * Использует циклическую навигацию
+ */
+ const handleNext = useCallback(() => {
+ setActivePeriod((prev) => (prev + 1) % totalPeriods)
+ }, [totalPeriods])
+
+ return (
+
+
Исторические даты
+
+
+
+ {currentPeriod.yearFrom}
+ {currentPeriod.yearTo}
+
+
+
{currentPeriod.label}
+
+
+
+
+
+ {String(activePeriod + 1).padStart(2, '0')}/
+ {String(totalPeriods).padStart(2, '0')}
+
+
+
+
+
+
+
+
+
+
+ )
+})
+
+TimeFrameSlider.displayName = 'TimeFrameSlider'
diff --git a/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/constants.ts b/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/constants.ts
new file mode 100644
index 0000000..737ede5
--- /dev/null
+++ b/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/constants.ts
@@ -0,0 +1,8 @@
+/**
+ * Константы для компонента TimeFrameSlider
+ */
+
+/**
+ * Угол позиции активного элемента (верхний правый угол)
+ */
+export const ACTIVE_POSITION_ANGLE = -60
diff --git a/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/index.ts b/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/index.ts
new file mode 100644
index 0000000..43600a7
--- /dev/null
+++ b/src/widgets/TimeFrameSlider/ui/TimeFrameSlider/index.ts
@@ -0,0 +1 @@
+export { TimeFrameSlider } from './TimeFrameSlider'