diff --git a/KNOWLEDGE_BASE.md b/KNOWLEDGE_BASE.md
index ba3e3c840..24482f93e 100644
--- a/KNOWLEDGE_BASE.md
+++ b/KNOWLEDGE_BASE.md
@@ -353,9 +353,9 @@ Settings → Appearance offers:
These appearance choices are saved in the browser, not on the account, so each browser keeps its own. Settings → Profile lets you choose one of six lane colors (Orange, Green, Violet, Teal, Rose, Ochre), a custom color, or the automatic color used around your identity and activity graph.
-Profile settings also let you share a timezone. Timestamps can show the author's local time as well as the viewer's when a timezone is shared. Leaving it unset uses the browser's timezone for your display without publishing your timezone. These /dev/null preferences are separate from your GitHub-managed profile information.
+Profile settings also let you share a timezone. Timestamps can show the author's local time as well as the viewer's when a timezone is shared. Leaving it unset uses the browser's timezone for your display without publishing your timezone. A separate, off-by-default setting shows your current local time on your profile, next to your location and website; it only applies while a timezone is set, and clearing the timezone turns it off. These /dev/null preferences are separate from your GitHub-managed profile information.
-Sources: [appearance settings](resources/js/pages/appearance/update.tsx), [lane picker](resources/js/components/lane-picker.tsx), [timezone picker](resources/js/components/timezone-picker.tsx), [timestamp tooltip](resources/js/components/timestamp-tooltip.tsx).
+Sources: [appearance settings](resources/js/pages/appearance/update.tsx), [lane picker](resources/js/components/lane-picker.tsx), [timezone picker](resources/js/components/timezone-picker.tsx), [local time](resources/js/components/local-time.tsx), [timestamp tooltip](resources/js/components/timestamp-tooltip.tsx).
### Installing /dev/null as an app
diff --git a/app/Actions/CreateExport.php b/app/Actions/CreateExport.php
index 2ae21603c..5b4f57520 100644
--- a/app/Actions/CreateExport.php
+++ b/app/Actions/CreateExport.php
@@ -50,6 +50,7 @@ public function handle(User $user): array
'has_sponsors_listing' => $user->has_sponsors_listing,
'preferred_lane' => $user->preferred_lane,
'timezone' => $user->timezone,
+ 'show_local_time' => $user->show_local_time,
'translation_language' => $user->translation_language,
'lane_color' => $user->lane_color,
'pasteis_mode' => $user->pasteis_mode,
diff --git a/app/Actions/UpdateUserTimezone.php b/app/Actions/UpdateUserTimezone.php
index 167eaa6f8..c66be990c 100644
--- a/app/Actions/UpdateUserTimezone.php
+++ b/app/Actions/UpdateUserTimezone.php
@@ -8,8 +8,11 @@
final readonly class UpdateUserTimezone
{
- public function handle(User $user, ?string $timezone): void
+ public function handle(User $user, ?string $timezone, bool $showLocalTime = false): void
{
- $user->update(['timezone' => $timezone]);
+ $user->update([
+ 'timezone' => $timezone,
+ 'show_local_time' => $timezone !== null && $showLocalTime,
+ ]);
}
}
diff --git a/app/Http/Controllers/UserTimezoneController.php b/app/Http/Controllers/UserTimezoneController.php
index c62e58109..5edfe7c61 100644
--- a/app/Http/Controllers/UserTimezoneController.php
+++ b/app/Http/Controllers/UserTimezoneController.php
@@ -15,7 +15,7 @@
{
public function update(UpdateUserTimezoneRequest $request, #[CurrentUser] User $user, UpdateUserTimezone $action): RedirectResponse
{
- $action->handle($user, $request->timezone());
+ $action->handle($user, $request->timezone(), $request->showLocalTime());
Inertia::flash('toast', [
'type' => 'success',
diff --git a/app/Http/Requests/UpdateUserTimezoneRequest.php b/app/Http/Requests/UpdateUserTimezoneRequest.php
index 9f4de87a5..a626e9020 100644
--- a/app/Http/Requests/UpdateUserTimezoneRequest.php
+++ b/app/Http/Requests/UpdateUserTimezoneRequest.php
@@ -20,6 +20,7 @@ public function rules(): array
{
return [
'timezone' => ['present', 'nullable', 'string', 'timezone:all'],
+ 'show_local_time' => ['nullable', 'boolean'],
];
}
@@ -27,4 +28,9 @@ public function timezone(): ?string
{
return $this->filled('timezone') ? $this->string('timezone')->value() : null;
}
+
+ public function showLocalTime(): bool
+ {
+ return $this->boolean('show_local_time');
+ }
}
diff --git a/app/Models/User.php b/app/Models/User.php
index a66f3680e..ee06d6c94 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -46,6 +46,7 @@
* @property-read string|null $pinned_commit_id
* @property-read string|null $translation_language
* @property-read string|null $timezone
+ * @property-read bool $show_local_time
* @property-read LivePlatform|null $live_platform
* @property-read string|null $live_url
* @property-read CarbonInterface|null $live_ends_at
@@ -122,6 +123,7 @@ final class User extends Authenticatable
protected $attributes = [
'log' => Log::Following->value,
'translation_language' => null,
+ 'show_local_time' => false,
'pasteis_mode' => false,
'remotes_enabled' => true,
'remotes_from' => RemotesFrom::Following->value,
@@ -171,6 +173,7 @@ public function casts(): array
'pasteis_given_on' => 'date',
'pinned_commit_id' => 'string',
'timezone' => 'string',
+ 'show_local_time' => 'boolean',
'translation_language' => 'encrypted',
'log' => Log::class,
'onboarded_at' => 'datetime',
diff --git a/database/migrations/2026_10_20_120000_add_show_local_time_to_users_table.php b/database/migrations/2026_10_20_120000_add_show_local_time_to_users_table.php
new file mode 100644
index 000000000..a282b83b5
--- /dev/null
+++ b/database/migrations/2026_10_20_120000_add_show_local_time_to_users_table.php
@@ -0,0 +1,17 @@
+boolean('show_local_time')->default(false);
+ });
+ }
+};
diff --git a/resources/js/components/local-time.tsx b/resources/js/components/local-time.tsx
new file mode 100644
index 000000000..3cece5c3a
--- /dev/null
+++ b/resources/js/components/local-time.tsx
@@ -0,0 +1,44 @@
+import { Clock } from 'lucide-react';
+import { useSyncExternalStore } from 'react';
+import { formatUtcOffset, formatZonedTime } from '@/lib/utils';
+
+const MINUTE = 60_000;
+
+function subscribe(onChange: () => void): () => void {
+ const interval = setInterval(onChange, MINUTE);
+
+ return () => clearInterval(interval);
+}
+
+/**
+ * Snapshots are whole minutes, so the clock re-renders when the minute turns rather than on every read.
+ */
+function currentMinute(): number {
+ return Math.floor(Date.now() / MINUTE) * MINUTE;
+}
+
+/**
+ * What time it is for someone who chose to share their timezone. It renders nothing on the server, where the
+ * current time is not the reader's.
+ */
+export function LocalTime({ timeZone }: { timeZone: string }) {
+ const minute = useSyncExternalStore(subscribe, currentMinute, () => null);
+
+ if (minute === null) {
+ return null;
+ }
+
+ const now = new Date(minute);
+
+ return (
+