{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "countdown",
  "title": "Countdown",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/countdown/countdown.component.ts",
      "content": "import {\n  Component,\n  ContentChild,\n  DestroyRef,\n  Directive,\n  EmbeddedViewRef,\n  EventEmitter,\n  Input,\n  OnChanges,\n  OnDestroy,\n  Output,\n  SimpleChanges,\n  TemplateRef,\n  ViewContainerRef,\n  ViewEncapsulation,\n  booleanAttribute,\n  inject,\n  signal,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { cn } from '@/lib/utils'\n\nexport type CountdownFormat = 'DD:HH:MM:SS' | 'HH:MM:SS' | 'MM:SS' | 'SS'\n\n/** Context of the whole-display template (React `children` render prop). */\nexport interface CountdownRenderProps {\n  days: number\n  hours: number\n  minutes: number\n  seconds: number\n  display: string\n  finished: boolean\n}\n\n/** Context of a per-unit template (React `renderDays` / `renderHours` / …): `let-days`. */\nexport interface CountdownUnitContext {\n  $implicit: number\n}\n\n// Copied verbatim from React Countdown (injected there once into <head>). Unscoped\n// (ViewEncapsulation.None) so the `[data-slot='countdown'] .countdown-digit` selectors match.\nconst COUNTDOWN_STYLES = `\n@keyframes countdown-digit-flip {\n  0% { opacity: 0; transform: translateY(45%) scale(0.92); }\n  100% { opacity: 1; transform: translateY(0) scale(1); }\n}\n[data-slot='countdown'] .countdown-digit {\n  display: inline-block;\n  animation: countdown-digit-flip 280ms cubic-bezier(0.22, 1, 0.36, 1) both;\n}\n@media (prefers-reduced-motion: reduce) {\n  [data-slot='countdown'] .countdown-digit {\n    animation: none !important;\n  }\n}\n`\n\n/** Renders a template with a context and keeps one view, updating its context on change. */\n@Directive({ selector: '[uiCountdownOutlet]', standalone: true })\nexport class UiCountdownOutletDirective<C extends object> implements OnChanges, OnDestroy {\n  @Input('uiCountdownOutlet') template: TemplateRef<C> | null = null\n  @Input('uiCountdownOutletContext') context!: C\n  private readonly vcr = inject(ViewContainerRef)\n  private view?: EmbeddedViewRef<C>\n\n  ngOnChanges(changes: SimpleChanges): void {\n    if (changes['template'] || !this.view) {\n      this.vcr.clear()\n      this.view = this.template ? this.vcr.createEmbeddedView(this.template, { ...this.context }) : undefined\n      return\n    }\n    Object.assign(this.view.context, this.context)\n    this.view.markForCheck()\n  }\n\n  ngOnDestroy(): void {\n    this.vcr.clear()\n  }\n}\n\n/**\n * Angular port of UIPKGE Countdown (React `Countdown`). Ticks every second toward `target`\n * (Date, ISO string or epoch ms): DD:HH:MM:SS / HH:MM:SS / MM:SS / SS (compact formats roll\n * larger units up) or a custom token format, optional label, leading-zero padding, custom\n * separator, `paused`, `tick` (remaining ms) and a one-shot `finish` — fired immediately when\n * the target is already past. Digits re-mount on change to replay the flip keyframes.\n * Per-unit templates (`renderDays` …) replace a unit; a projected `<ng-template let-p>`\n * replaces the whole display (React `children`). State is signal-backed (zoneless timers).\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-countdown, [ui-countdown]',\n  standalone: true,\n  imports: [UiCountdownOutletDirective],\n  encapsulation: ViewEncapsulation.None,\n  styles: [COUNTDOWN_STYLES],\n  host: {\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-slot]': '\"countdown\"',\n    '[attr.data-finished]': 'finished() ? \"true\" : \"false\"',\n    '[attr.data-paused]': 'paused ? \"true\" : \"false\"',\n    '[class]': 'hostClass',\n  },\n  template: `\n    @if (label) {\n      <span data-slot=\"countdown-label\" class=\"text-muted-foreground text-xs font-medium tracking-wide uppercase\">{{\n        label\n      }}</span>\n    }\n    <div\n      data-slot=\"countdown-display\"\n      class=\"flex items-baseline gap-1 font-mono tabular-nums\"\n      role=\"timer\"\n      [attr.aria-live]=\"finished() || paused ? 'off' : 'polite'\"\n      aria-atomic=\"true\"\n    >\n      @if (childrenTemplate) {\n        <ng-container [uiCountdownOutlet]=\"childrenTemplate\" [uiCountdownOutletContext]=\"renderContext\" />\n      } @else {\n        @if (has('DD')) {\n          @if (renderDays) {\n            <ng-container [uiCountdownOutlet]=\"renderDays\" [uiCountdownOutletContext]=\"{ $implicit: parts.days }\" />\n          } @else {\n            <span data-slot=\"countdown-days\" class=\"text-foreground inline-flex text-2xl font-semibold\">\n              @for (ch of digits(displayParts.days); track $index + '-' + ch) {\n                <span class=\"countdown-digit tabular-nums\">{{ ch }}</span>\n              }\n            </span>\n          }\n        }\n        @if (has('DD') && has('HH')) {\n          <span class=\"text-muted-foreground text-2xl\">{{ separator }}</span>\n        }\n        @if (has('HH')) {\n          @if (renderHours) {\n            <ng-container [uiCountdownOutlet]=\"renderHours\" [uiCountdownOutletContext]=\"{ $implicit: parts.hours }\" />\n          } @else {\n            <span data-slot=\"countdown-hours\" class=\"text-foreground inline-flex text-2xl font-semibold\">\n              @for (ch of digits(displayParts.hours); track $index + '-' + ch) {\n                <span class=\"countdown-digit tabular-nums\">{{ ch }}</span>\n              }\n            </span>\n          }\n        }\n        @if (has('HH') && has('MM')) {\n          <span class=\"text-muted-foreground text-2xl\">{{ separator }}</span>\n        }\n        @if (has('MM')) {\n          @if (renderMinutes) {\n            <ng-container\n              [uiCountdownOutlet]=\"renderMinutes\"\n              [uiCountdownOutletContext]=\"{ $implicit: parts.minutes }\"\n            />\n          } @else {\n            <span data-slot=\"countdown-minutes\" class=\"text-foreground inline-flex text-2xl font-semibold\">\n              @for (ch of digits(displayParts.minutes); track $index + '-' + ch) {\n                <span class=\"countdown-digit tabular-nums\">{{ ch }}</span>\n              }\n            </span>\n          }\n        }\n        @if (has('MM') && has('SS')) {\n          <span class=\"text-muted-foreground text-2xl\">{{ separator }}</span>\n        }\n        @if (has('SS')) {\n          @if (renderSeconds) {\n            <ng-container\n              [uiCountdownOutlet]=\"renderSeconds\"\n              [uiCountdownOutletContext]=\"{ $implicit: parts.seconds }\"\n            />\n          } @else {\n            <span data-slot=\"countdown-seconds\" class=\"text-foreground inline-flex text-2xl font-semibold\">\n              @for (ch of digits(displayParts.seconds); track $index + '-' + ch) {\n                <span class=\"countdown-digit tabular-nums\">{{ ch }}</span>\n              }\n            </span>\n          }\n        }\n      }\n    </div>\n  `,\n})\nexport class UiCountdownComponent implements OnChanges {\n  /** Target date/time. Accepts a Date, ISO string, or epoch ms number. */\n  @Input({ required: true }) target!: Date | string | number\n  /** Display format. Custom tokens: DD days, HH hours, MM minutes, SS seconds. */\n  @Input() format: CountdownFormat | string = 'DD:HH:MM:SS'\n  /** Pause the countdown. */\n  @Input({ transform: booleanAttribute }) paused = false\n  /** Optional label rendered above the countdown. */\n  @Input() label = ''\n  /** Show leading zeros (e.g. 05 vs 5). */\n  @Input({ transform: booleanAttribute }) pad = true\n  /** Separator between units. */\n  @Input() separator = ':'\n  /** Per-unit template overrides (`<ng-template #d let-days>`). */\n  @Input() renderDays?: TemplateRef<CountdownUnitContext> | null\n  @Input() renderHours?: TemplateRef<CountdownUnitContext> | null\n  @Input() renderMinutes?: TemplateRef<CountdownUnitContext> | null\n  @Input() renderSeconds?: TemplateRef<CountdownUnitContext> | null\n  @Input('class') className?: string\n  /** Fired once when the countdown reaches zero. */\n  @Output() readonly finish = new EventEmitter<void>()\n  /** Fired every tick with the remaining ms. */\n  @Output() readonly tick = new EventEmitter<number>()\n\n  /** Whole-display override (React `children` render prop): a projected `<ng-template let-p>`. */\n  @ContentChild(TemplateRef) childrenTemplate?: TemplateRef<{ $implicit: CountdownRenderProps }>\n\n  readonly now = signal(Date.now())\n  readonly finished = signal(false)\n  private finishedFlag = false\n  private timer: ReturnType<typeof setInterval> | null = null\n  private lastTargetMs: number | undefined\n\n  constructor() {\n    inject(DestroyRef).onDestroy(() => this.stop())\n  }\n\n  get hostClass(): string {\n    return cn('inline-flex flex-col gap-1', this.className)\n  }\n\n  get targetMs(): number {\n    const t = this.target\n    if (t instanceof Date) return t.getTime()\n    if (typeof t === 'number') return t\n    return new Date(t).getTime()\n  }\n\n  get remainingMs(): number {\n    return Math.max(0, this.targetMs - this.now())\n  }\n\n  get parts(): { days: number; hours: number; minutes: number; seconds: number } {\n    const total = this.remainingMs\n    return {\n      days: Math.floor(total / 86_400_000),\n      hours: Math.floor((total % 86_400_000) / 3_600_000),\n      minutes: Math.floor((total % 3_600_000) / 60_000),\n      seconds: Math.floor((total % 60_000) / 1000),\n    }\n  }\n\n  /** Values painted for each unit under the active format (rolled-up totals for compact formats). */\n  get displayParts(): { days: number; hours: number; minutes: number; seconds: number } {\n    const { days, hours, minutes, seconds } = this.parts\n    if (this.format === 'HH:MM:SS') return { days, hours: days * 24 + hours, minutes, seconds }\n    if (this.format === 'MM:SS') return { days, hours, minutes: days * 24 * 60 + hours * 60 + minutes, seconds }\n    if (this.format === 'SS') return { days, hours, minutes, seconds: Math.floor(this.remainingMs / 1000) }\n    return { days, hours, minutes, seconds }\n  }\n\n  get display(): string {\n    const f = this.format\n    const { days, hours, minutes, seconds } = this.displayParts\n    const sep = this.separator\n    const p = (n: number) => this.pad2(n)\n    if (f === 'DD:HH:MM:SS') return `${p(days)}${sep}${p(hours)}${sep}${p(minutes)}${sep}${p(seconds)}`\n    if (f === 'HH:MM:SS') return `${p(hours)}${sep}${p(minutes)}${sep}${p(seconds)}`\n    if (f === 'MM:SS') return `${p(minutes)}${sep}${p(seconds)}`\n    if (f === 'SS') return p(seconds)\n    const parts = this.parts\n    return f\n      .replace('DD', p(parts.days))\n      .replace('HH', p(parts.hours))\n      .replace('MM', p(parts.minutes))\n      .replace('SS', p(parts.seconds))\n  }\n\n  get renderContext(): { $implicit: CountdownRenderProps } {\n    return { $implicit: { ...this.parts, display: this.display, finished: this.finished() } }\n  }\n\n  has(token: 'DD' | 'HH' | 'MM' | 'SS'): boolean {\n    return this.format.includes(token)\n  }\n\n  pad2(n: number): string {\n    return this.pad ? String(n).padStart(2, '0') : String(n)\n  }\n\n  digits(n: number): string[] {\n    return [...this.pad2(n)]\n  }\n\n  ngOnChanges(changes: SimpleChanges): void {\n    const targetMs = this.targetMs\n    if (targetMs !== this.lastTargetMs) {\n      // Reset finished state only when the target changes.\n      this.lastTargetMs = targetMs\n      this.finishedFlag = false\n      this.finished.set(false)\n      this.now.set(Date.now())\n      this.restart()\n    } else if (changes['paused']) {\n      this.restart()\n    }\n  }\n\n  private stop(): void {\n    if (this.timer !== null) clearInterval(this.timer)\n    this.timer = null\n  }\n\n  private markFinished(): void {\n    if (this.finishedFlag) return\n    this.finishedFlag = true\n    this.finished.set(true)\n    this.finish.emit()\n  }\n\n  private restart(): void {\n    this.stop()\n    const targetMs = this.targetMs\n    // Fire finish immediately when the target is already past (don't wait for first tick).\n    if (Math.max(0, targetMs - Date.now()) <= 0) {\n      this.markFinished()\n      return\n    }\n    if (this.paused) return\n    this.timer = setInterval(() => {\n      const next = Date.now()\n      const remaining = Math.max(0, targetMs - next)\n      this.tick.emit(remaining)\n      this.now.set(next)\n      if (remaining <= 0) {\n        this.markFinished()\n        this.stop()\n      }\n    }, 1000)\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/countdown/countdown.component.ts"
    },
    {
      "path": "packages/registry-angular/components/countdown/index.ts",
      "content": "export {\n  UiCountdownComponent,\n  UiCountdownOutletDirective,\n  type CountdownFormat,\n  type CountdownRenderProps,\n  type CountdownUnitContext,\n} from './countdown.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/countdown/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Countdown timer showing time remaining to a target date. DD:HH:MM:SS, HH:MM:SS, MM:SS, and SS formats plus custom token formats, label, leading-zero padding, custom separator, paused state, finish / tick events, digit flip animation, and per-unit or whole-display template overrides.",
  "categories": [
    "display",
    "utility"
  ]
}