Skip to content
Brok UI

Loading…

No results

Clock Widget

Open source

Live clocks — an analog dial with arabic/roman/no numerals and a multi-city world clock.

Preview

1 of 2

resources/views/blocks/clock-widget-analog.blade.php Blade
@props([
    'numerals' => 'arabic',
])

@php
    // Numeral sets. The 12 labels are laid out clockwise starting at the 1
    // o'clock position; index 11 (the XII / 12 slot) sits at the top.
    $numerals = in_array($numerals, ['arabic', 'roman', 'none'], true) ? $numerals : 'arabic';

    $sets = [
        'arabic' => ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
        'roman' => ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI', 'XII'],
    ];

    // Pre-compute each numeral's position as a percentage from the dial centre
    // so it can ride on the card without a colour or layout dependency.
    $marks = [];
    if ($numerals !== 'none') {
        $labels = $sets[$numerals];
        $radius = 40; // percent of the dial radius the labels sit at
        foreach ($labels as $i => $label) {
            $hour = $i + 1;
            $angle = ($hour / 12) * 360;
            $radians = deg2rad($angle);
            $x = 50 + sin($radians) * $radius;
            $y = 50 - cos($radians) * $radius;
            $marks[] = ['label' => $label, 'x' => round($x, 4), 'y' => round($y, 4)];
        }
    }
@endphp

<x-ui.card class="w-full max-w-[15rem] aspect-square flex items-center justify-center">
    <div
        x-data="{
            now: null,
            timer: null,
            init() {
                this.tick();
                this.timer = setInterval(() => this.tick(), 1000);
            },
            destroy() {
                clearInterval(this.timer);
            },
            tick() {
                this.now = new Date();
            },
            get hourDeg() {
                if (!this.now) return 0;
                const h = this.now.getHours() % 12;
                const m = this.now.getMinutes();
                return ((h + m / 60) / 12) * 360;
            },
            get minuteDeg() {
                if (!this.now) return 0;
                const m = this.now.getMinutes();
                const s = this.now.getSeconds();
                return ((m + s / 60) / 60) * 360;
            },
            get secondDeg() {
                if (!this.now) return 0;
                return (this.now.getSeconds() / 60) * 360;
            },
        }"
        role="img"
        aria-label="{{ __('Analog clock') }}"
        class="relative aspect-square w-[80%] rounded-full border border-border bg-card"
    >
        {{-- Numerals --}}
        @foreach ($marks as $mark)
            <span
                class="absolute text-sm font-semibold text-foreground"
                style="top: {{ $mark['y'] }}%; inset-inline-start: {{ $mark['x'] }}%; transform: translate(-50%, -50%);"
                aria-hidden="true"
            >{{ $mark['label'] }}</span>
        @endforeach

        {{-- Hour hand (thick) --}}
        <div
            class="absolute bottom-1/2 left-1/2 h-[28%] w-1 origin-bottom rounded-full bg-foreground"
            style="transform: translateX(-50%) rotate(0deg);"
            :style="`transform: translateX(-50%) rotate(${hourDeg}deg)`"
            aria-hidden="true"
        ></div>

        {{-- Minute hand --}}
        <div
            class="absolute bottom-1/2 left-1/2 h-[38%] w-1 origin-bottom rounded-full bg-foreground"
            style="transform: translateX(-50%) rotate(0deg);"
            :style="`transform: translateX(-50%) rotate(${minuteDeg}deg)`"
            aria-hidden="true"
        ></div>

        {{-- Second hand (thin) --}}
        <div
            class="absolute bottom-1/2 left-1/2 h-[42%] w-0.5 origin-bottom rounded-full bg-destructive"
            style="transform: translateX(-50%) rotate(0deg);"
            :style="`transform: translateX(-50%) rotate(${secondDeg}deg)`"
            aria-hidden="true"
        ></div>

        {{-- Centre dot --}}
        <div
            class="absolute top-1/2 left-1/2 size-3 -translate-x-1/2 -translate-y-1/2 rounded-full bg-foreground"
            aria-hidden="true"
        ></div>
    </div>
</x-ui.card>

Installation

terminal
php artisan ui:add blocks/clock-widget-analog

Installs these dependencies automatically:

Registry contract

Install confidence

php artisan ui:add blocks/clock-widget-analog writes only the generated targets below. The CLI validates each file hash before writing and prompts before replacing local changes unless --force is used.

Version
1.0.2
License
open
Stability
stable
Contract
v1
Foundation
≥ 1.0.0
Type Generated target
blade resources/views/blocks/clock-widget-analog.blade.php

Registry dependencies

card

Package dependencies

composer: jml/brok:^0.2

npm: alpinejs

Use with AI

A brief for your coding agent: what the block is, the install command, how to render it, its props and the rules. Copy it, or open a prompt about this block in an assistant.

clock-widget-analog.md
# Brok UI block: Clock Widget Analog (`clock-widget-analog`)

A live-ticking analog clock widget inside a square card: a bordered dial with optional arabic, roman, or no numerals, a thick hour hand, a minute hand, a thin destructive-toned second hand, and a centre dot, all driven from the local time in inline Alpine.

Brok UI is a Laravel Blade registry. `ui:add` copies this block into the app as plain Blade the app owns; it composes installed `<brok:*>` primitives and semantic design tokens.

## Install

```bash
php artisan ui:add blocks/clock-widget-analog
```

## Render it

```blade
<x-blocks.clock-widget-analog />
```

## Props

- `numerals` (string, default `arabic`)

## Use when

- Displaying a live-ticking analog clock face with hour, minute and second hands driven from the visitor's local time.
- Choosing between arabic numerals, roman numerals or a numeral-free dial via the numerals prop.

## Avoid when

- Use clock-widget-world for a multi-city world-clock list instead of a single analog dial.

## Rules

- Render the installed block with `<x-blocks.clock-widget-analog />` and pass data through its props; edit the copied file only for structural changes.
- Keep the semantic design tokens (`bg-background`, `text-muted-foreground`); never swap in raw colour utilities.
- Keep the `data-slot` attributes and the logical (start/end) spacing so the markup still mirrors under `dir="rtl"`.

## Links

- Docs: https://brokui.dev/blocks/clock-widget-analog
- Registry JSON (files, props, contract): https://brokui.dev/r/open/blocks/clock-widget-analog.json

Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.

Guidance

Use when

  • Displaying a live-ticking analog clock face with hour, minute and second hands driven from the visitor's local time.
  • Choosing between arabic numerals, roman numerals or a numeral-free dial via the numerals prop.

Avoid when

  • Use clock-widget-world for a multi-city world-clock list instead of a single analog dial.

Anti-patterns

  • Do not replace semantic props with conflicting utility classes.
  • Do not remove labels, focus styles, or state attributes.

Usage

Render the block as a component, passing data where useful:

resources/views/welcome.blade.php Blade
<x-blocks.clock-widget-analog />

Built from primitives

Composes <brok:*> Semantic tokens Re-themeable RTL-ready

This block composes installed <brok:*> primitives and semantic design tokens only — it does not reimplement any primitive. Re-theme it (light, dark, admin, customer) by editing CSS variables; flip the preview to RTL to confirm it mirrors.

Source

The exact, editable file ui:add writes into your app. The preview above renders this same source — there are no preview-only blocks.

resources/views/blocks/clock-widget-analog.blade.php Blade
@props([
    'numerals' => 'arabic',
])

@php
    // Numeral sets. The 12 labels are laid out clockwise starting at the 1
    // o'clock position; index 11 (the XII / 12 slot) sits at the top.
    $numerals = in_array($numerals, ['arabic', 'roman', 'none'], true) ? $numerals : 'arabic';

    $sets = [
        'arabic' => ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
        'roman' => ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI', 'XII'],
    ];

    // Pre-compute each numeral's position as a percentage from the dial centre
    // so it can ride on the card without a colour or layout dependency.
    $marks = [];
    if ($numerals !== 'none') {
        $labels = $sets[$numerals];
        $radius = 40; // percent of the dial radius the labels sit at
        foreach ($labels as $i => $label) {
            $hour = $i + 1;
            $angle = ($hour / 12) * 360;
            $radians = deg2rad($angle);
            $x = 50 + sin($radians) * $radius;
            $y = 50 - cos($radians) * $radius;
            $marks[] = ['label' => $label, 'x' => round($x, 4), 'y' => round($y, 4)];
        }
    }
@endphp

<x-ui.card class="w-full max-w-[15rem] aspect-square flex items-center justify-center">
    <div
        x-data="{
            now: null,
            timer: null,
            init() {
                this.tick();
                this.timer = setInterval(() => this.tick(), 1000);
            },
            destroy() {
                clearInterval(this.timer);
            },
            tick() {
                this.now = new Date();
            },
            get hourDeg() {
                if (!this.now) return 0;
                const h = this.now.getHours() % 12;
                const m = this.now.getMinutes();
                return ((h + m / 60) / 12) * 360;
            },
            get minuteDeg() {
                if (!this.now) return 0;
                const m = this.now.getMinutes();
                const s = this.now.getSeconds();
                return ((m + s / 60) / 60) * 360;
            },
            get secondDeg() {
                if (!this.now) return 0;
                return (this.now.getSeconds() / 60) * 360;
            },
        }"
        role="img"
        aria-label="{{ __('Analog clock') }}"
        class="relative aspect-square w-[80%] rounded-full border border-border bg-card"
    >
        {{-- Numerals --}}
        @foreach ($marks as $mark)
            <span
                class="absolute text-sm font-semibold text-foreground"
                style="top: {{ $mark['y'] }}%; inset-inline-start: {{ $mark['x'] }}%; transform: translate(-50%, -50%);"
                aria-hidden="true"
            >{{ $mark['label'] }}</span>
        @endforeach

        {{-- Hour hand (thick) --}}
        <div
            class="absolute bottom-1/2 left-1/2 h-[28%] w-1 origin-bottom rounded-full bg-foreground"
            style="transform: translateX(-50%) rotate(0deg);"
            :style="`transform: translateX(-50%) rotate(${hourDeg}deg)`"
            aria-hidden="true"
        ></div>

        {{-- Minute hand --}}
        <div
            class="absolute bottom-1/2 left-1/2 h-[38%] w-1 origin-bottom rounded-full bg-foreground"
            style="transform: translateX(-50%) rotate(0deg);"
            :style="`transform: translateX(-50%) rotate(${minuteDeg}deg)`"
            aria-hidden="true"
        ></div>

        {{-- Second hand (thin) --}}
        <div
            class="absolute bottom-1/2 left-1/2 h-[42%] w-0.5 origin-bottom rounded-full bg-destructive"
            style="transform: translateX(-50%) rotate(0deg);"
            :style="`transform: translateX(-50%) rotate(${secondDeg}deg)`"
            aria-hidden="true"
        ></div>

        {{-- Centre dot --}}
        <div
            class="absolute top-1/2 left-1/2 size-3 -translate-x-1/2 -translate-y-1/2 rounded-full bg-foreground"
            aria-hidden="true"
        ></div>
    </div>
</x-ui.card>

Ownership & lifecycle

Owner, release state, review evidence and adoption for this item.
Owner
Platform UI (@JoshJML)
Current version
1.0.2
Status
Stable
License
open
Deprecation
Not deprecated
Contract
v1
Foundation
≥ 1.0.0