UIPackage
Menu

Framework

Change language

Boilerplate repo

Pomodoro Focus Timer

blockproductivity

Pomodoro productivity workspace with focus and break interval timers, SVG circular progress countdown display, active task queue with pomodoro estimate badges, ambient sound generator switcher, and daily focus analytics metrics.

Also available for Vue ->

Installation

$npx shadcn@latest add https://uipkge.dev/r/react/pomodoro-focus-timer.json
Named registry:npx shadcn@latest add @uipkge-react/pomodoro-focus-timerInstalls to:components/blocks/

Variants

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
initialModeTimerModeoptional
initialTimenumberoptional
initialSoundstringoptional
initialSessionnumberoptional
classNamestringoptional

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

FocusTask
interface FocusTask {
  id: string
  title: string
  tag: string
  done: boolean
  currentPomodoros: number
  targetPomodoros: number
}

Files installed (1)

  • components/blocks/PomodoroFocusTimer.tsx30.5 kB
    'use client'
    
    import * as React from 'react'
    import {
      BarChart3,
      Check,
      Flame,
      Headphones,
      ListTodo,
      Music2,
      Pause,
      Play,
      Plus,
      RotateCcw,
      SkipForward,
      Target,
      Trash2,
      Trees,
      Volume2,
      VolumeX,
      Waves,
    } from 'lucide-react'
    import { cn } from '@/lib/utils'
    import { Badge } from '@/components/ui/badge'
    import { Button } from '@/components/ui/button'
    import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
    import { Checkbox } from '@/components/ui/checkbox'
    import { Input } from '@/components/ui/input'
    import { Progress } from '@/components/ui/progress'
    import { Separator } from '@/components/ui/separator'
    
    export interface FocusTask {
      id: string
      title: string
      tag: string
      done: boolean
      currentPomodoros: number
      targetPomodoros: number
    }
    
    export type TimerMode = 'focus' | 'shortBreak' | 'longBreak'
    
    export interface PomodoroFocusTimerProps {
      initialMode?: TimerMode
      initialTime?: number
      initialSound?: string
      initialSession?: number
      className?: string
    }
    
    const MODE_DURATIONS: Record<TimerMode, number> = {
      focus: 25 * 60,
      shortBreak: 5 * 60,
      longBreak: 15 * 60,
    }
    
    const ambientSounds = [
      { id: 'off', name: 'Off / Mute', icon: VolumeX, label: 'Silent' },
      { id: 'whitenoise', name: 'Deep Focus White Noise', icon: Waves, label: 'White Noise' },
      { id: 'lofi', name: 'Lofi Beats', icon: Music2, label: 'Lofi Beats' },
      { id: 'rainforest', name: 'Rainforest', icon: Trees, label: 'Rainforest' },
    ]
    
    const initialTasks: FocusTask[] = [
      {
        id: 'task-1',
        title: 'Refactor OKLCH color-mix utilities in Tailwind v4',
        tag: 'Design System',
        done: false,
        currentPomodoros: 2,
        targetPomodoros: 3,
      },
      {
        id: 'task-2',
        title: 'Audit ARIA accessibility roles on interactive primitives',
        tag: 'Accessibility',
        done: true,
        currentPomodoros: 2,
        targetPomodoros: 2,
      },
      {
        id: 'task-3',
        title: 'Draft technical RFC for component variant registry',
        tag: 'Architecture',
        done: false,
        currentPomodoros: 0,
        targetPomodoros: 2,
      },
      {
        id: 'task-4',
        title: 'Benchmark Astro island hydration bundle sizes',
        tag: 'Performance',
        done: false,
        currentPomodoros: 1,
        targetPomodoros: 4,
      },
    ]
    
    export function PomodoroFocusTimer({
      initialMode = 'focus',
      initialTime = 1485, // 24:45
      initialSound = 'lofi',
      initialSession = 3,
      className,
    }: PomodoroFocusTimerProps) {
      const [currentMode, setCurrentMode] = React.useState<TimerMode>(initialMode)
      const [remainingSeconds, setRemainingSeconds] = React.useState<number>(initialTime)
      const [isRunning, setIsRunning] = React.useState<boolean>(false)
      const [currentSession, setCurrentSession] = React.useState<number>(initialSession)
      const [completedSessionsToday, setCompletedSessionsToday] = React.useState<number>(8)
      const [selectedSound, setSelectedSound] = React.useState<string>(initialSound)
      const [isSoundMuted, setIsSoundMuted] = React.useState<boolean>(false)
      const [tasks, setTasks] = React.useState<FocusTask[]>(initialTasks)
      const [activeTaskId, setActiveTaskId] = React.useState<string>('task-1')
      const [newTaskTitle, setNewTaskTitle] = React.useState<string>('')
      const [newTaskEstimate, setNewTaskEstimate] = React.useState<number>(2)
    
      // Timer interval countdown
      React.useEffect(() => {
        let interval: NodeJS.Timeout | null = null
        if (isRunning) {
          interval = setInterval(() => {
            setRemainingSeconds((prev) => {
              if (prev <= 1) {
                handleSkipPhase()
                return 0
              }
              return prev - 1
            })
          }, 1000)
        }
        return () => {
          if (interval) clearInterval(interval)
        }
      }, [isRunning, currentMode, currentSession, activeTaskId])
    
      const activeTask = tasks.find((t) => t.id === activeTaskId) ||
        tasks.find((t) => !t.done) || {
          id: 'default',
          title: 'General Deep Work Session',
          tag: 'Productivity',
          done: false,
          currentPomodoros: 0,
          targetPomodoros: 1,
        }
    
      const completedTasksCount = tasks.filter((t) => t.done).length
      const totalModeSeconds = MODE_DURATIONS[currentMode]
      const progressPercentage = Math.min(
        100,
        Math.max(0, ((totalModeSeconds - remainingSeconds) / totalModeSeconds) * 100),
      )
    
      const RADIUS = 104
      const CIRCUMFERENCE = 2 * Math.PI * RADIUS
      const strokeDashoffset = CIRCUMFERENCE * (1 - progressPercentage / 100)
    
      function formatTime(seconds: number): string {
        const mins = Math.floor(seconds / 60)
        const secs = seconds % 60
        return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`
      }
    
      function handleToggleTimer() {
        setIsRunning((prev) => !prev)
      }
    
      function handleResetTimer() {
        setIsRunning(false)
        setRemainingSeconds(MODE_DURATIONS[currentMode])
      }
    
      function handleSetMode(mode: TimerMode) {
        setIsRunning(false)
        setCurrentMode(mode)
        setRemainingSeconds(MODE_DURATIONS[mode])
      }
    
      function handleSkipPhase() {
        setIsRunning(false)
        if (currentMode === 'focus') {
          setCompletedSessionsToday((prev) => prev + 1)
          setTasks((prev) =>
            prev.map((t) =>
              t.id === activeTaskId && t.currentPomodoros < t.targetPomodoros
                ? { ...t, currentPomodoros: t.currentPomodoros + 1 }
                : t,
            ),
          )
          if (currentSession >= 4) {
            setCurrentMode('longBreak')
            setCurrentSession(1)
            setRemainingSeconds(MODE_DURATIONS.longBreak)
          } else {
            setCurrentMode('shortBreak')
            setCurrentSession((prev) => prev + 1)
            setRemainingSeconds(MODE_DURATIONS.shortBreak)
          }
        } else {
          setCurrentMode('focus')
          setRemainingSeconds(MODE_DURATIONS.focus)
        }
      }
    
      function handleToggleTask(taskId: string) {
        setTasks((prev) =>
          prev.map((t) => {
            if (t.id === taskId) {
              const nextDone = !t.done
              return {
                ...t,
                done: nextDone,
                currentPomodoros:
                  nextDone && t.currentPomodoros < t.targetPomodoros ? t.targetPomodoros : t.currentPomodoros,
              }
            }
            return t
          }),
        )
      }
    
      function handleRemoveTask(taskId: string) {
        setTasks((prev) => prev.filter((t) => t.id !== taskId))
        if (activeTaskId === taskId) {
          const remaining = tasks.filter((t) => t.id !== taskId)
          if (remaining.length > 0) {
            setActiveTaskId(remaining[0].id)
          }
        }
      }
    
      function handleAddTask(e: React.FormEvent) {
        e.preventDefault()
        const trimmed = newTaskTitle.trim()
        if (!trimmed) return
        const id = `task-${Date.now()}`
        const newTask: FocusTask = {
          id,
          title: trimmed,
          tag: 'Focus',
          done: false,
          currentPomodoros: 0,
          targetPomodoros: Math.max(1, newTaskEstimate || 2),
        }
        setTasks((prev) => [...prev, newTask])
        setNewTaskTitle('')
        setNewTaskEstimate(2)
        if (!activeTaskId || tasks.length === 0) {
          setActiveTaskId(id)
        }
      }
    
      function handleToggleSoundMute() {
        setIsSoundMuted((prev) => !prev)
      }
    
      return (
        <div data-slot="pomodoro-focus-timer" className={cn('mx-auto w-full max-w-5xl space-y-6', className)}>
          {/* Header Section */}
          <header className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
            <div className="space-y-1">
              <div className="flex items-center gap-2">
                <h2 className="text-foreground text-2xl font-bold tracking-tight sm:text-3xl">
                  Focus Space &amp; Pomodoro Timer
                </h2>
                <Badge variant="secondary" className="font-mono text-xs">
                  v2.4
                </Badge>
              </div>
              <p className="text-muted-foreground text-sm">
                High-yield interval productivity engine with ambient soundscapes and active task execution.
              </p>
            </div>
    
            <div className="flex flex-wrap items-center gap-2">
              {/* Daily Streak Badge */}
              <Badge
                wrap
                variant="outline"
                className="border-warning/30 bg-warning/10 text-warning gap-1.5 px-3 py-1.5 text-xs font-semibold"
              >
                <Flame className="fill-warning text-warning size-4" />
                <span>🔥 6-Day Focus Streak · {completedSessionsToday} Sessions Complete Today</span>
              </Badge>
            </div>
          </header>
    
          {/* Ambient Sound Preset Bar */}
          <Card>
            <CardContent className="flex flex-wrap items-center justify-between gap-3 p-3 sm:p-4">
              <div className="text-muted-foreground flex items-center gap-2 text-xs font-medium">
                <Headphones className="text-primary size-4" />
                <span className="text-foreground font-semibold">Ambient Sound:</span>
                <span className="hidden sm:inline">Choose your background audio focus generator</span>
              </div>
    
              <div className="flex flex-wrap items-center gap-1.5">
                {ambientSounds.map((sound) => {
                  const Icon = sound.icon
                  const isCurrent = selectedSound === sound.id && !isSoundMuted
                  return (
                    <Button
                      key={sound.id}
                      variant={isCurrent ? 'default' : 'outline'}
                      size="sm"
                      className="h-8 gap-1.5 text-xs"
                      onClick={() => {
                        setSelectedSound(sound.id)
                        if (sound.id === 'off') {
                          setIsSoundMuted(true)
                        } else {
                          setIsSoundMuted(false)
                        }
                      }}
                    >
                      <Icon className="size-3.5" />
                      <span>{sound.name}</span>
                      {isCurrent && sound.id !== 'off' && (
                        <span className="flex items-center gap-0.5">
                          <span className="bg-primary-foreground h-2 w-0.5 animate-pulse" />
                          <span className="bg-primary-foreground h-3.5 w-0.5 animate-pulse delay-75" />
                          <span className="bg-primary-foreground h-1.5 w-0.5 animate-pulse delay-150" />
                        </span>
                      )}
                    </Button>
                  )
                })}
    
                <Button
                  variant="ghost"
                  size="icon-sm"
                  className="text-muted-foreground hover:text-foreground size-8"
                  title={isSoundMuted ? 'Unmute Ambient Sound' : 'Mute Ambient Sound'}
                  onClick={handleToggleSoundMute}
                >
                  {isSoundMuted ? <VolumeX className="text-destructive size-4" /> : <Volume2 className="size-4" />}
                </Button>
              </div>
            </CardContent>
          </Card>
    
          {/* Main Grid: Timer Centerpiece (Left) and Tasks + Analytics (Right) */}
          <div className="grid grid-cols-1 items-start gap-6 lg:grid-cols-12">
            {/* Left Column: Centerpiece Timer */}
            <div className="space-y-6 lg:col-span-6">
              {/* Timer Card */}
              <Card className="border-border relative overflow-hidden shadow-xs">
                {/* Card Header / Mode Switcher */}
                <CardHeader className="pb-2 text-center">
                  <div className="bg-muted/70 mx-auto flex w-full max-w-sm items-center justify-center rounded-xl p-1">
                    <Button
                      variant={currentMode === 'focus' ? 'default' : 'ghost'}
                      size="sm"
                      className="flex-1 rounded-lg text-xs font-semibold"
                      onClick={() => handleSetMode('focus')}
                    >
                      Focus 25m
                    </Button>
                    <Button
                      variant={currentMode === 'shortBreak' ? 'default' : 'ghost'}
                      size="sm"
                      className="flex-1 rounded-lg text-xs font-semibold"
                      onClick={() => handleSetMode('shortBreak')}
                    >
                      Short Break 5m
                    </Button>
                    <Button
                      variant={currentMode === 'longBreak' ? 'default' : 'ghost'}
                      size="sm"
                      className="flex-1 rounded-lg text-xs font-semibold"
                      onClick={() => handleSetMode('longBreak')}
                    >
                      Long Break 15m
                    </Button>
                  </div>
                </CardHeader>
    
                {/* Card Content / Circular Countdown Display */}
                <CardContent className="flex flex-col items-center justify-center pt-4 pb-2">
                  {/* Circular SVG Ring Frame */}
                  <div className="relative flex size-64 items-center justify-center sm:size-72">
                    <svg className="size-full -rotate-90 transform" viewBox="0 0 256 256">
                      {/* Background track */}
                      <circle
                        cx="128"
                        cy="128"
                        r={RADIUS}
                        fill="transparent"
                        stroke="currentColor"
                        strokeWidth="10"
                        className="text-muted/40"
                      />
                      {/* Progress ring */}
                      <circle
                        cx="128"
                        cy="128"
                        r={RADIUS}
                        fill="transparent"
                        stroke="currentColor"
                        strokeWidth="10"
                        strokeLinecap="round"
                        strokeDasharray={CIRCUMFERENCE}
                        strokeDashoffset={strokeDashoffset}
                        className={cn(
                          'transition-colors duration-700 ease-out motion-reduce:transition-none',
                          currentMode === 'focus' && 'text-primary',
                          currentMode === 'shortBreak' && 'text-success',
                          currentMode === 'longBreak' && 'text-info',
                        )}
                      />
                    </svg>
    
                    {/* Center Countdown Display */}
                    <div className="absolute inset-0 flex flex-col items-center justify-center text-center">
                      <div className="flex items-center gap-1.5">
                        <span
                          className={cn(
                            'size-2 rounded-full',
                            isRunning ? 'animate-ping' : 'opacity-60',
                            currentMode === 'focus' && 'bg-primary',
                            currentMode === 'shortBreak' && 'bg-success',
                            currentMode === 'longBreak' && 'bg-info',
                          )}
                        />
                        <span
                          className={cn(
                            'text-xs font-semibold tracking-wider uppercase',
                            currentMode === 'focus' && 'text-primary',
                            currentMode === 'shortBreak' && 'text-success',
                            currentMode === 'longBreak' && 'text-info',
                          )}
                        >
                          {currentMode === 'focus'
                            ? 'Deep Focus'
                            : currentMode === 'shortBreak'
                              ? 'Short Break'
                              : 'Rest Phase'}
                        </span>
                      </div>
    
                      {/* Big Countdown Readout */}
                      <div className="text-foreground my-1 font-mono text-5xl font-bold tracking-tight tabular-nums sm:text-6xl">
                        {formatTime(remainingSeconds)}
                      </div>
    
                      {/* Current State Label */}
                      <div className="text-muted-foreground text-xs font-medium">
                        {isRunning ? `Session ${currentSession} of 4 active` : 'Timer Paused'}
                      </div>
                    </div>
                  </div>
    
                  {/* Current Active Task Banner */}
                  <div className="border-border/70 bg-muted/40 mt-4 flex w-full max-w-sm items-center justify-between gap-2 rounded-lg border p-2.5 text-xs">
                    <div className="flex min-w-0 flex-wrap items-center gap-2">
                      <Target className="text-primary size-4 shrink-0" />
                      <div className="min-w-0 flex-1">
                        <span className="text-foreground block truncate font-medium">{activeTask.title}</span>
                      </div>
                    </div>
                    <Badge wrap variant="secondary" className="shrink-0 gap-1 text-xs">
                      <span>
                        🍅 {activeTask.currentPomodoros}/{activeTask.targetPomodoros}
                      </span>
                    </Badge>
                  </div>
                </CardContent>
    
                {/* Action Controls & Session Dots */}
                <CardFooter className="flex flex-col gap-4 pt-2">
                  {/* Action Buttons Row */}
                  <div className="flex items-center justify-center gap-4">
                    {/* Reset Button */}
                    <Button
                      variant="outline"
                      size="icon"
                      className="text-muted-foreground hover:text-foreground size-11 rounded-full"
                      title="Reset current interval"
                      onClick={handleResetTimer}
                      aria-label="Action"
                    >
                      <RotateCcw className="size-4" />
                    </Button>
    
                    {/* Primary Play / Pause Button */}
                    <Button
                      variant="default"
                      size="icon-lg"
                      className="size-14 rounded-full shadow-md transition-[color,background-color,border-color,box-shadow,opacity,transform,scale,translate,rotate] active:scale-95"
                      title={isRunning ? 'Pause Timer' : 'Start Focus Timer'}
                      onClick={handleToggleTimer}
                    >
                      {isRunning ? (
                        <Pause className="size-6 fill-current" />
                      ) : (
                        <Play className="ml-0.5 size-6 fill-current" />
                      )}
                    </Button>
    
                    {/* Skip Button */}
                    <Button
                      variant="outline"
                      size="icon"
                      className="text-muted-foreground hover:text-foreground size-11 rounded-full"
                      title="Skip to next phase"
                      onClick={handleSkipPhase}
                      aria-label="Action"
                    >
                      <SkipForward className="size-4" />
                    </Button>
                  </div>
    
                  <Separator />
    
                  {/* Session Tracker 4-Dot Indicator */}
                  <div className="text-muted-foreground flex w-full items-center justify-between text-xs">
                    <span className="text-foreground font-medium">Session {currentSession} of 4</span>
    
                    <div className="flex items-center gap-2">
                      {[1, 2, 3, 4].map((index) => (
                        <div
                          key={index}
                          className={cn(
                            'flex size-4 items-center justify-center rounded-full transition-colors',
                            index < currentSession
                              ? 'bg-primary text-primary-foreground'
                              : index === currentSession
                                ? 'ring-primary ring-offset-background bg-primary/80 ring-2 ring-offset-2'
                                : 'border-muted-foreground/30 bg-muted/30 border-2',
                          )}
                        >
                          {index < currentSession && <Check className="size-2.5 stroke-[3]" />}
                        </div>
                      ))}
                    </div>
    
                    <span>Long break after 4</span>
                  </div>
                </CardFooter>
              </Card>
    
              {/* Daily Focus Analytics Card */}
              <Card className="border-border shadow-xs">
                <CardHeader className="pb-3">
                  <div className="flex items-center justify-between">
                    <div className="flex items-center gap-2">
                      <BarChart3 className="text-primary size-4" />
                      <CardTitle className="text-base font-semibold">Daily Focus Analytics</CardTitle>
                    </div>
                    <Badge wrap variant="outline" className="text-muted-foreground text-xs font-normal">
                      Today
                    </Badge>
                  </div>
                  <CardDescription className="text-xs">
                    Aggregated productivity stats and session completion rate
                  </CardDescription>
                </CardHeader>
    
                <CardContent className="space-y-4">
                  {/* 3 Mini Metric Tiles */}
                  <div className="grid grid-cols-3 gap-3">
                    <div className="border-border/60 bg-muted/30 rounded-lg border p-3 text-center">
                      <div className="text-muted-foreground text-xs font-medium">Total Focus</div>
                      <div className="text-foreground mt-1 font-mono text-lg font-bold tabular-nums">3h 45m</div>
                      <div className="text-success mt-0.5 text-xs">+28m vs avg</div>
                    </div>
    
                    <div className="border-border/60 bg-muted/30 rounded-lg border p-3 text-center">
                      <div className="text-muted-foreground text-xs font-medium">Completed</div>
                      <div className="text-foreground mt-1 font-mono text-lg font-bold tabular-nums">
                        {completedSessionsToday} <span className="text-muted-foreground text-xs font-normal">/ 10</span>
                      </div>
                      <div className="text-muted-foreground mt-0.5 text-xs">80% of goal</div>
                    </div>
    
                    <div className="border-border/60 bg-muted/30 rounded-lg border p-3 text-center">
                      <div className="text-muted-foreground text-xs font-medium">Focus Score</div>
                      <div className="text-foreground mt-1 font-mono text-lg font-bold tabular-nums">94%</div>
                      <div className="text-primary mt-0.5 text-xs font-semibold">High Flow</div>
                    </div>
                  </div>
    
                  {/* Daily Goal Progress */}
                  <div className="space-y-1.5">
                    <div className="flex items-center justify-between text-xs">
                      <span className="text-muted-foreground">Daily Session Goal</span>
                      <span className="text-foreground font-semibold tabular-nums">{completedSessionsToday}/10 (80%)</span>
                    </div>
                    <Progress value={80} className="h-2" />
                  </div>
    
                  <Separator />
    
                  {/* Recent Sessions History */}
                  <div className="space-y-2">
                    <div className="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
                      Recent Completed Intervals
                    </div>
                    <div className="space-y-1.5 text-xs">
                      <div className="bg-muted/30 flex items-center justify-between rounded-md px-2.5 py-1.5">
                        <div className="flex items-center gap-2 truncate">
                          <span className="text-muted-foreground font-mono tabular-nums">10:45 AM</span>
                          <span className="text-foreground truncate font-medium">Refactor OKLCH color-mix</span>
                        </div>
                        <Badge wrap variant="success" className="h-5 px-1.5 text-xs">
                          25m Focus
                        </Badge>
                      </div>
                      <div className="bg-muted/30 flex items-center justify-between rounded-md px-2.5 py-1.5">
                        <div className="flex items-center gap-2 truncate">
                          <span className="text-muted-foreground font-mono tabular-nums">10:15 AM</span>
                          <span className="text-muted-foreground truncate">Short Break · Lofi Beats</span>
                        </div>
                        <Badge wrap variant="secondary" className="h-5 px-1.5 text-xs">
                          5m Rest
                        </Badge>
                      </div>
                      <div className="bg-muted/30 flex items-center justify-between rounded-md px-2.5 py-1.5">
                        <div className="flex items-center gap-2 truncate">
                          <span className="text-muted-foreground font-mono tabular-nums">09:30 AM</span>
                          <span className="text-foreground truncate font-medium">Audit ARIA roles</span>
                        </div>
                        <Badge wrap variant="success" className="h-5 px-1.5 text-xs">
                          25m Focus
                        </Badge>
                      </div>
                    </div>
                  </div>
                </CardContent>
              </Card>
            </div>
    
            {/* Right Column: Active Task Queue */}
            <div className="space-y-6 lg:col-span-6">
              <Card className="border-border shadow-xs">
                <CardHeader className="pb-3">
                  <div className="flex items-center justify-between">
                    <div className="flex items-center gap-2">
                      <ListTodo className="text-primary size-4" />
                      <CardTitle className="text-base font-semibold">Active Focus Task Queue</CardTitle>
                    </div>
                    <Badge wrap variant="secondary" className="text-xs">
                      {completedTasksCount}/{tasks.length} Completed
                    </Badge>
                  </div>
                  <CardDescription className="text-xs">
                    Select an item to link it to the active timer countdown. Check off tasks as you finish.
                  </CardDescription>
                </CardHeader>
    
                <CardContent className="space-y-4">
                  {/* Add New Task Form */}
                  <form className="flex items-center gap-2" onSubmit={handleAddTask}>
                    <div className="flex-1">
                      <Input
                        value={newTaskTitle}
                        onChange={(e) => setNewTaskTitle(e.target.value)}
                        placeholder="Add a new focus task..."
                        size="middle"
                        className="text-xs"
                      />
                    </div>
                    <div className="w-20">
                      <Input
                        value={newTaskEstimate}
                        onChange={(e) => setNewTaskEstimate(Number(e.target.value))}
                        type="number"
                        min={1}
                        max={12}
                        placeholder="🍅 Est"
                        size="middle"
                        className="text-center text-xs tabular-nums"
                        title="Estimated Pomodoros"
                      />
                    </div>
                    <Button type="submit" size="default" className="gap-1 text-xs">
                      <Plus className="size-3.5" />
                      <span>Add</span>
                    </Button>
                  </form>
    
                  <Separator />
    
                  {/* Tasks List */}
                  <div className="space-y-2.5">
                    {tasks.map((task) => (
                      <div
                        key={task.id}
                        className={cn(
                          'group flex items-center justify-between gap-3 rounded-lg border p-3 transition-colors',
                          task.id === activeTaskId && !task.done
                            ? 'border-primary/50 bg-primary/5 shadow-xs'
                            : 'border-border/70 bg-card hover:border-border hover:bg-muted/30',
                          task.done && 'bg-muted/20 opacity-65',
                        )}
                      >
                        {/* Task Checkbox + Title */}
                        <div className="flex min-w-0 flex-1 items-start gap-3">
                          <div className="pt-0.5">
                            <Checkbox checked={task.done} onCheckedChange={() => handleToggleTask(task.id)} />
                          </div>
    
                          <button
                            type="button"
                            className="focus-visible:ring-ring min-w-0 flex-1 cursor-pointer rounded-md text-left focus-visible:ring-2 focus-visible:outline-none"
                            aria-pressed={task.id === activeTaskId}
                            onClick={() => setActiveTaskId(task.id)}
                          >
                            <div className="flex items-center gap-2">
                              <p
                                className={cn(
                                  'text-foreground text-xs font-medium transition-colors',
                                  task.done && 'text-muted-foreground line-through',
                                )}
                              >
                                {task.title}
                              </p>
                              {task.id === activeTaskId && !task.done && (
                                <Badge wrap variant="default" className="h-4.5 px-1.5 text-xs">
                                  Active
                                </Badge>
                              )}
                            </div>
    
                            <div className="text-muted-foreground mt-1 flex items-center gap-2 text-xs">
                              <Badge wrap variant="outline" className="h-4 px-1 text-xs font-normal">
                                {task.tag}
                              </Badge>
                              <span>Estimated: {task.targetPomodoros * 25}m</span>
                            </div>
                          </button>
                        </div>
    
                        {/* Pomodoro Pills + Delete */}
                        <div className="flex items-center gap-2">
                          <Badge
                            wrap
                            variant={task.done ? 'secondary' : task.id === activeTaskId ? 'default' : 'outline'}
                            className="shrink-0 gap-1 text-xs font-medium tabular-nums"
                          >
                            <span>
                              🍅 {task.currentPomodoros}/{task.targetPomodoros}
                            </span>
                          </Badge>
    
                          <Button
                            variant="ghost"
                            size="icon-sm"
                            className="text-muted-foreground hover:text-destructive size-7 opacity-0 group-hover:opacity-100"
                            title="Remove task"
                            onClick={() => handleRemoveTask(task.id)}
                          >
                            <Trash2 className="size-3.5" />
                          </Button>
                        </div>
                      </div>
                    ))}
                  </div>
                </CardContent>
    
                <CardFooter className="border-border/50 bg-muted/20 text-muted-foreground flex items-center justify-between border-t py-3 text-xs">
                  <span>Tip: Switch tasks any time without resetting timer elapsed time.</span>
                  <Button
                    variant="ghost"
                    size="sm"
                    className="text-muted-foreground hover:text-foreground h-7 text-xs"
                    onClick={() => setTasks((prev) => prev.filter((t) => !t.done))}
                  >
                    Clear Done ({completedTasksCount})
                  </Button>
                </CardFooter>
              </Card>
            </div>
          </div>
        </div>
      )
    }
    

Raw manifest:https://uipkge.dev/r/react/pomodoro-focus-timer.json