diff --git a/assets/audio/complete.wav b/assets/audio/complete.wav
new file mode 100644
index 0000000..8a4dac7
Binary files /dev/null and b/assets/audio/complete.wav differ
diff --git a/assets/audio/countdown.wav b/assets/audio/countdown.wav
new file mode 100644
index 0000000..4540ad3
Binary files /dev/null and b/assets/audio/countdown.wav differ
diff --git a/assets/audio/phase-start.wav b/assets/audio/phase-start.wav
new file mode 100644
index 0000000..9db0128
Binary files /dev/null and b/assets/audio/phase-start.wav differ
diff --git a/src/shared/components/CLAUDE.md b/src/shared/components/CLAUDE.md
new file mode 100644
index 0000000..1003e4f
--- /dev/null
+++ b/src/shared/components/CLAUDE.md
@@ -0,0 +1,11 @@
+
+# Recent Activity
+
+
+
+### Feb 18, 2026
+
+| ID | Time | T | Title | Read |
+|----|------|---|-------|------|
+| #4889 | 4:46 PM | 🟣 | Created GlassCard component with iOS 18.4 inspired glassmorphism | ~174 |
+
\ No newline at end of file
diff --git a/src/shared/components/GlassCard.tsx b/src/shared/components/GlassCard.tsx
new file mode 100644
index 0000000..f1c992f
--- /dev/null
+++ b/src/shared/components/GlassCard.tsx
@@ -0,0 +1,105 @@
+/**
+ * GlassCard - Liquid Glass Container
+ * iOS 18.4 inspired glassmorphism
+ */
+
+import { ReactNode } from 'react'
+import { StyleSheet, View, ViewStyle } from 'react-native'
+import { BlurView } from 'expo-blur'
+
+import { DARK, GLASS, SHADOW, BORDER } from '../constants/colors'
+import { RADIUS } from '../constants/borderRadius'
+
+type GlassVariant = 'base' | 'elevated' | 'inset' | 'tinted'
+
+interface GlassCardProps {
+ children: ReactNode
+ variant?: GlassVariant
+ style?: ViewStyle
+ hasBlur?: boolean
+ blurIntensity?: number
+}
+
+const variantStyles: Record = {
+ base: {
+ backgroundColor: GLASS.BASE.backgroundColor,
+ borderColor: GLASS.BASE.borderColor,
+ borderWidth: GLASS.BASE.borderWidth,
+ },
+ elevated: {
+ backgroundColor: GLASS.ELEVATED.backgroundColor,
+ borderColor: GLASS.ELEVATED.borderColor,
+ borderWidth: GLASS.ELEVATED.borderWidth,
+ },
+ inset: {
+ backgroundColor: GLASS.INSET.backgroundColor,
+ borderColor: GLASS.INSET.borderColor,
+ borderWidth: GLASS.INSET.borderWidth,
+ },
+ tinted: {
+ backgroundColor: GLASS.TINTED.backgroundColor,
+ borderColor: GLASS.TINTED.borderColor,
+ borderWidth: GLASS.TINTED.borderWidth,
+ },
+}
+
+const shadowStyles: Record = {
+ base: SHADOW.sm,
+ elevated: SHADOW.md,
+ inset: {},
+ tinted: SHADOW.sm,
+}
+
+export function GlassCard({
+ children,
+ variant = 'base',
+ style,
+ hasBlur = true,
+ blurIntensity = GLASS.BLUR_MEDIUM,
+}: GlassCardProps) {
+ const glassStyle = variantStyles[variant]
+ const shadowStyle = shadowStyles[variant]
+
+ if (hasBlur) {
+ return (
+
+
+ {children}
+
+ )
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+
+const styles = StyleSheet.create({
+ container: {
+ borderRadius: RADIUS.GLASS_CARD,
+ overflow: 'hidden',
+ },
+ content: {
+ flex: 1,
+ },
+})
+
+// Preset components for common use cases
+
+export function GlassCardElevated(props: Omit) {
+ return
+}
+
+export function GlassCardInset(props: Omit) {
+ return
+}
+
+export function GlassCardTinted(props: Omit) {
+ return
+}
diff --git a/src/shared/components/StyledText.tsx b/src/shared/components/StyledText.tsx
new file mode 100644
index 0000000..c761203
--- /dev/null
+++ b/src/shared/components/StyledText.tsx
@@ -0,0 +1,50 @@
+/**
+ * TabataFit StyledText
+ * Unified text component — replaces 5 local copies
+ */
+
+import { Text as RNText, TextStyle, StyleProp } from 'react-native'
+import { TEXT } from '../constants/colors'
+
+type FontWeight = 'regular' | 'medium' | 'semibold' | 'bold'
+
+const WEIGHT_MAP: Record = {
+ regular: '400',
+ medium: '500',
+ semibold: '600',
+ bold: '700',
+}
+
+interface StyledTextProps {
+ children: React.ReactNode
+ size?: number
+ weight?: FontWeight
+ color?: string
+ style?: StyleProp
+ numberOfLines?: number
+}
+
+export function StyledText({
+ children,
+ size = 17,
+ weight = 'regular',
+ color = TEXT.PRIMARY,
+ style,
+ numberOfLines,
+}: StyledTextProps) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/shared/components/VideoPlayer.tsx b/src/shared/components/VideoPlayer.tsx
new file mode 100644
index 0000000..214b09f
--- /dev/null
+++ b/src/shared/components/VideoPlayer.tsx
@@ -0,0 +1,78 @@
+/**
+ * TabataFit VideoPlayer Component
+ * Looping muted preview mode (detail) + full-screen background mode (player)
+ * Falls back to gradient when no video URL
+ */
+
+import { useRef, useEffect, useCallback } from 'react'
+import { View, StyleSheet } from 'react-native'
+import { useVideoPlayer, VideoView } from 'expo-video'
+import { LinearGradient } from 'expo-linear-gradient'
+import { BRAND } from '../constants/colors'
+
+interface VideoPlayerProps {
+ /** HLS or MP4 video URL */
+ videoUrl?: string
+ /** Fallback gradient colors when no video */
+ gradientColors?: readonly [string, string, ...string[]]
+ /** Looping muted preview (detail) or full-screen background (player) */
+ mode?: 'preview' | 'background'
+ /** Whether to play the video */
+ isPlaying?: boolean
+ style?: object
+}
+
+export function VideoPlayer({
+ videoUrl,
+ gradientColors = [BRAND.PRIMARY, BRAND.PRIMARY_DARK],
+ mode = 'preview',
+ isPlaying = true,
+ style,
+}: VideoPlayerProps) {
+ const player = useVideoPlayer(videoUrl ?? null, (p) => {
+ p.loop = true
+ p.muted = mode === 'preview'
+ p.volume = mode === 'background' ? 0.3 : 0
+ })
+
+ useEffect(() => {
+ if (!player || !videoUrl) return
+ if (isPlaying) {
+ player.play()
+ } else {
+ player.pause()
+ }
+ }, [isPlaying, player, videoUrl])
+
+ // No video URL — show gradient fallback
+ if (!videoUrl) {
+ return (
+
+
+
+ )
+ }
+
+ return (
+
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ container: {
+ overflow: 'hidden',
+ backgroundColor: '#000',
+ },
+})
diff --git a/src/shared/hooks/index.ts b/src/shared/hooks/index.ts
new file mode 100644
index 0000000..a7e7e08
--- /dev/null
+++ b/src/shared/hooks/index.ts
@@ -0,0 +1,8 @@
+/**
+ * TabataFit Shared Hooks
+ */
+
+export { useTimer } from './useTimer'
+export type { TimerPhase } from './useTimer'
+export { useHaptics } from './useHaptics'
+export { useAudio } from './useAudio'
diff --git a/src/shared/hooks/useAudio.ts b/src/shared/hooks/useAudio.ts
new file mode 100644
index 0000000..e0a899e
--- /dev/null
+++ b/src/shared/hooks/useAudio.ts
@@ -0,0 +1,74 @@
+/**
+ * TabataFit Audio Hook
+ * Sound effects for timer events using expo-av
+ * Respects userStore soundEffects setting
+ */
+
+import { useRef, useEffect, useCallback } from 'react'
+import { Audio } from 'expo-av'
+import { useUserStore } from '../stores'
+
+// Audio assets
+const SOUNDS = {
+ countdown: require('../../../assets/audio/countdown.wav'),
+ phaseStart: require('../../../assets/audio/phase-start.wav'),
+ complete: require('../../../assets/audio/complete.wav'),
+}
+
+type SoundKey = keyof typeof SOUNDS
+
+export function useAudio() {
+ const soundEnabled = useUserStore((s) => s.settings.soundEffects)
+ const loadedSounds = useRef>({})
+
+ // Configure audio session
+ useEffect(() => {
+ Audio.setAudioModeAsync({
+ playsInSilentModeIOS: true,
+ staysActiveInBackground: false,
+ shouldDuckAndroid: true,
+ })
+
+ return () => {
+ // Unload all sounds on cleanup
+ Object.values(loadedSounds.current).forEach(sound => {
+ sound.unloadAsync().catch(() => {})
+ })
+ loadedSounds.current = {}
+ }
+ }, [])
+
+ const getSound = useCallback(async (key: SoundKey): Promise => {
+ if (loadedSounds.current[key]) {
+ return loadedSounds.current[key]
+ }
+ try {
+ const { sound } = await Audio.Sound.createAsync(SOUNDS[key])
+ loadedSounds.current[key] = sound
+ return sound
+ } catch {
+ return null
+ }
+ }, [])
+
+ const play = useCallback(async (key: SoundKey) => {
+ if (!soundEnabled) return
+ const sound = await getSound(key)
+ if (!sound) return
+ try {
+ await sound.setPositionAsync(0)
+ await sound.playAsync()
+ } catch {
+ // Sound may have been unloaded
+ }
+ }, [soundEnabled, getSound])
+
+ return {
+ /** Short beep for countdown ticks (3, 2, 1) */
+ countdownBeep: useCallback(() => play('countdown'), [play]),
+ /** Ding for phase transitions (work → rest, rest → work) */
+ phaseStart: useCallback(() => play('phaseStart'), [play]),
+ /** Celebration chime on workout completion */
+ workoutComplete: useCallback(() => play('complete'), [play]),
+ }
+}
diff --git a/src/shared/hooks/useHaptics.ts b/src/shared/hooks/useHaptics.ts
new file mode 100644
index 0000000..9a48916
--- /dev/null
+++ b/src/shared/hooks/useHaptics.ts
@@ -0,0 +1,45 @@
+/**
+ * TabataFit Haptics Hook
+ * Centralized haptic feedback respecting user settings
+ */
+
+import { useCallback } from 'react'
+import * as Haptics from 'expo-haptics'
+import { useUserStore } from '../stores'
+
+export function useHaptics() {
+ const haptics = useUserStore((s) => s.settings.haptics)
+
+ const phaseChange = useCallback(() => {
+ if (!haptics) return
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy)
+ }, [haptics])
+
+ const buttonTap = useCallback(() => {
+ if (!haptics) return
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium)
+ }, [haptics])
+
+ const countdownTick = useCallback(() => {
+ if (!haptics) return
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)
+ }, [haptics])
+
+ const workoutComplete = useCallback(() => {
+ if (!haptics) return
+ Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success)
+ }, [haptics])
+
+ const selection = useCallback(() => {
+ if (!haptics) return
+ Haptics.selectionAsync()
+ }, [haptics])
+
+ return {
+ phaseChange,
+ buttonTap,
+ countdownTick,
+ workoutComplete,
+ selection,
+ }
+}
diff --git a/src/shared/hooks/useTimer.ts b/src/shared/hooks/useTimer.ts
new file mode 100644
index 0000000..4273a6c
--- /dev/null
+++ b/src/shared/hooks/useTimer.ts
@@ -0,0 +1,170 @@
+/**
+ * TabataFit Timer Hook
+ * Extracted from player/[id].tsx — reusable timer logic
+ */
+
+import { useRef, useEffect, useCallback } from 'react'
+import { usePlayerStore } from '../stores'
+import type { Workout } from '../types'
+
+export type TimerPhase = 'PREP' | 'WORK' | 'REST' | 'COMPLETE'
+
+interface UseTimerReturn {
+ phase: TimerPhase
+ timeRemaining: number
+ currentRound: number
+ totalRounds: number
+ currentExercise: string
+ nextExercise: string | undefined
+ progress: number
+ isPaused: boolean
+ isRunning: boolean
+ isComplete: boolean
+ calories: number
+ start: () => void
+ pause: () => void
+ resume: () => void
+ skip: () => void
+ stop: () => void
+}
+
+export function useTimer(workout: Workout | null): UseTimerReturn {
+ const store = usePlayerStore()
+ const intervalRef = useRef | null>(null)
+
+ // Load workout into store on mount
+ useEffect(() => {
+ if (workout) {
+ store.loadWorkout(workout)
+ }
+ return () => {
+ if (intervalRef.current) clearInterval(intervalRef.current)
+ }
+ }, [workout?.id])
+
+ const w = workout ?? {
+ prepTime: 10,
+ workTime: 20,
+ restTime: 10,
+ rounds: 8,
+ exercises: [{ name: 'Ready', duration: 20 }],
+ }
+
+ // Calculate phase duration
+ const phaseDuration =
+ store.phase === 'PREP' ? w.prepTime :
+ store.phase === 'WORK' ? w.workTime :
+ store.phase === 'REST' ? w.restTime : 0
+
+ const progress = phaseDuration > 0 ? 1 - store.timeRemaining / phaseDuration : 1
+
+ // Exercise index based on current round
+ const exerciseIndex = (store.currentRound - 1) % w.exercises.length
+ const currentExercise = w.exercises[exerciseIndex]?.name ?? ''
+ const nextExercise = w.exercises[(exerciseIndex + 1) % w.exercises.length]?.name
+
+ // Timer tick
+ useEffect(() => {
+ if (!store.isRunning || store.isPaused || store.phase === 'COMPLETE') {
+ if (intervalRef.current) {
+ clearInterval(intervalRef.current)
+ intervalRef.current = null
+ }
+ return
+ }
+
+ intervalRef.current = setInterval(() => {
+ const s = usePlayerStore.getState()
+ if (s.timeRemaining <= 1) {
+ // Phase transition
+ if (s.phase === 'PREP') {
+ store.setPhase('WORK')
+ store.setTimeRemaining(w.workTime)
+ } else if (s.phase === 'WORK') {
+ // Add calories for completed work phase
+ const caloriesPerRound = workout ? Math.round(workout.calories / workout.rounds) : 5
+ store.addCalories(caloriesPerRound)
+
+ store.setPhase('REST')
+ store.setTimeRemaining(w.restTime)
+ } else if (s.phase === 'REST') {
+ if (s.currentRound >= (workout?.rounds ?? 8)) {
+ store.setPhase('COMPLETE')
+ store.setTimeRemaining(0)
+ store.setRunning(false)
+ } else {
+ store.setPhase('WORK')
+ store.setTimeRemaining(w.workTime)
+ store.setCurrentRound(s.currentRound + 1)
+ }
+ }
+ } else {
+ store.setTimeRemaining(s.timeRemaining - 1)
+ }
+ }, 1000)
+
+ return () => {
+ if (intervalRef.current) {
+ clearInterval(intervalRef.current)
+ intervalRef.current = null
+ }
+ }
+ }, [store.isRunning, store.isPaused, store.phase])
+
+ const start = useCallback(() => {
+ store.setRunning(true)
+ store.setPaused(false)
+ }, [])
+
+ const pause = useCallback(() => {
+ store.setPaused(true)
+ }, [])
+
+ const resume = useCallback(() => {
+ store.setPaused(false)
+ }, [])
+
+ const skip = useCallback(() => {
+ const s = usePlayerStore.getState()
+ if (s.phase === 'PREP') {
+ store.setPhase('WORK')
+ store.setTimeRemaining(w.workTime)
+ } else if (s.phase === 'WORK') {
+ store.setPhase('REST')
+ store.setTimeRemaining(w.restTime)
+ } else if (s.phase === 'REST') {
+ if (s.currentRound >= (workout?.rounds ?? 8)) {
+ store.setPhase('COMPLETE')
+ store.setTimeRemaining(0)
+ store.setRunning(false)
+ } else {
+ store.setPhase('WORK')
+ store.setTimeRemaining(w.workTime)
+ store.setCurrentRound(s.currentRound + 1)
+ }
+ }
+ }, [])
+
+ const stop = useCallback(() => {
+ store.reset()
+ }, [])
+
+ return {
+ phase: store.phase as TimerPhase,
+ timeRemaining: store.timeRemaining,
+ currentRound: store.currentRound,
+ totalRounds: workout?.rounds ?? 8,
+ currentExercise,
+ nextExercise: store.phase === 'REST' ? nextExercise : undefined,
+ progress,
+ isPaused: store.isPaused,
+ isRunning: store.isRunning,
+ isComplete: store.phase === 'COMPLETE',
+ calories: store.calories,
+ start,
+ pause,
+ resume,
+ skip,
+ stop,
+ }
+}