{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "local-time",
  "title": "实时当地时间",
  "description": "按指定 IANA 时区显示实时时间：行内时间可带 UTC 偏移、昼夜图标与同访客的时差；另提供世界时钟（列表 / 卡片）和平滑转动的 SVG 模拟表盘。",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "files": [
    {
      "path": "src/registry/ui/local-time/local-time.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useNow } from \"@/hooks/use-now\"\n\nimport { AnalogClock, type AnalogClockProps } from \"./analog-clock\"\nimport { DEFAULT_LABELS, type LocalTimeLabels } from \"./config\"\nimport { DayNightMark, TimeDigits } from \"./time-parts\"\nimport {\n  WorldClock,\n  WorldClockItem,\n  type WorldClockItemProps,\n  type WorldClockProps,\n  type WorldClockVariant,\n  type WorldClockZone,\n} from \"./world-clock\"\nimport {\n  clockParts,\n  describeDifference,\n  hourDifference,\n  isDaytime,\n  offsetMinutes,\n  utcLabel,\n  zoneHour,\n} from \"./zone\"\n\ntype LocalTimeProps = Omit<React.ComponentProps<\"span\">, \"children\"> & {\n  /** IANA 时区名，例如 `Asia/Shanghai`，不支持 `+08:00` 这类固定偏移 */\n  timeZone: string\n  /**\n   * BCP 47 区域标识，决定时间与数字的格式\n   * @default \"zh-CN\"\n   */\n  locale?: string\n  /**\n   * 12 小时制（带上午/下午），默认 24 小时制\n   * @default false\n   */\n  hour12?: boolean\n  /**\n   * 显示秒；关闭后每分钟才刷新一次\n   * @default true\n   */\n  showSeconds?: boolean\n  /**\n   * 在 `//` 后显示与访客时区的时差\n   * @default true\n   */\n  showDifference?: boolean\n  /**\n   * 在时间后显示 UTC 偏移，例如 `UTC+8`\n   * @default false\n   */\n  showZone?: boolean\n  /**\n   * 在时间前显示昼夜图标（6:00–17:59 为白天）\n   * @default false\n   */\n  showDayNight?: boolean\n  /**\n   * 数字变化时播放轻微的下落淡入；系统开启“减少动态效果”时自动关闭\n   * @default false\n   */\n  animated?: boolean\n  /** 覆盖部分可见文案 */\n  labels?: Partial<LocalTimeLabels>\n  /**\n   * 挂载前的占位时间；默认随 `showSeconds` 为 `--:--:--` 或 `--:--`\n   */\n  placeholder?: string\n  /** @deprecated 等价于 `labels.pending` */\n  placeholderLabel?: string\n  /** @deprecated 等价于 `labels.same` */\n  sameTimeLabel?: string\n  /** @deprecated 等价于 `labels.ahead` */\n  aheadLabel?: string\n  /** @deprecated 等价于 `labels.behind` */\n  behindLabel?: string\n  /** @deprecated 等价于 `labels.hourUnit` */\n  hourUnit?: string\n}\n\n/** 以指定 IANA 时区显示时间，并可附带 UTC 偏移、昼夜图标与同访客的时差。 */\nfunction LocalTime({\n  timeZone,\n  className,\n  locale = \"zh-CN\",\n  hour12 = false,\n  showSeconds = true,\n  showDifference = true,\n  showZone = false,\n  showDayNight = false,\n  animated = false,\n  labels,\n  placeholder,\n  placeholderLabel,\n  sameTimeLabel,\n  aheadLabel,\n  behindLabel,\n  hourUnit,\n  ...props\n}: LocalTimeProps) {\n  // 不显示秒时按分钟对齐，减少无谓的重新渲染\n  const now = useNow(showSeconds ? 1000 : 60_000)\n  const text = {\n    ...DEFAULT_LABELS,\n    ...labels,\n    ...(placeholderLabel !== undefined && { pending: placeholderLabel }),\n    ...(sameTimeLabel !== undefined && { same: sameTimeLabel }),\n    ...(aheadLabel !== undefined && { ahead: aheadLabel }),\n    ...(behindLabel !== undefined && { behind: behindLabel }),\n    ...(hourUnit !== undefined && { hourUnit }),\n  }\n\n  if (now === null)\n    return (\n      <span\n        data-slot=\"local-time\"\n        data-state=\"pending\"\n        className={cn(\"tabular-nums\", className)}\n        {...props}\n      >\n        {showDayNight ? (\n          <span aria-hidden=\"true\" className=\"mr-1.5 inline-block size-[1em]\" />\n        ) : null}\n        {placeholder ?? (showSeconds ? \"--:--:--\" : \"--:--\")}\n        {showDifference ? (\n          <>\n            {\" \"}\n            <span\n              data-slot=\"local-time-note\"\n              className=\"text-muted-foreground\"\n            >{`// ${text.pending}`}</span>\n          </>\n        ) : null}\n      </span>\n    )\n\n  const date = new Date(now)\n  const daylight = isDaytime(zoneHour(timeZone, date))\n  return (\n    <span\n      data-slot=\"local-time\"\n      data-state=\"ready\"\n      data-daylight={showDayNight ? (daylight ? \"day\" : \"night\") : undefined}\n      className={cn(\"tabular-nums\", className)}\n      {...props}\n    >\n      {showDayNight ? (\n        <DayNightMark\n          daylight={daylight}\n          label={daylight ? text.day : text.night}\n          className=\"mr-1.5\"\n        />\n      ) : null}\n      <time data-slot=\"local-time-time\" dateTime={date.toISOString()}>\n        <TimeDigits\n          parts={clockParts(date, { locale, timeZone, hour12, showSeconds })}\n          animated={animated}\n        />\n      </time>\n      {showZone ? (\n        <>\n          {\" \"}\n          <span data-slot=\"local-time-zone\" className=\"text-muted-foreground\">\n            {utcLabel(offsetMinutes(timeZone, date))}\n          </span>\n        </>\n      ) : null}\n      {showDifference ? (\n        <>\n          {\" \"}\n          <span\n            data-slot=\"local-time-note\"\n            className=\"text-muted-foreground\"\n          >{`// ${describeDifference(hourDifference(timeZone, date), locale, text)}`}</span>\n        </>\n      ) : null}\n    </span>\n  )\n}\n\nexport { AnalogClock, LocalTime, WorldClock, WorldClockItem }\nexport type {\n  AnalogClockProps,\n  LocalTimeLabels,\n  LocalTimeProps,\n  WorldClockItemProps,\n  WorldClockProps,\n  WorldClockVariant,\n  WorldClockZone,\n}\n",
      "type": "registry:ui",
      "target": "@ui/local-time/local-time.tsx"
    },
    {
      "path": "src/registry/ui/local-time/world-clock.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useNow } from \"@/hooks/use-now\"\n\nimport { AnalogClock } from \"./analog-clock\"\nimport { DEFAULT_LABELS, type LocalTimeLabels } from \"./config\"\nimport { DayNightMark, TimeDigits } from \"./time-parts\"\nimport {\n  clockParts,\n  dayOffset,\n  describeDifference,\n  hourDifference,\n  isDaytime,\n  offsetMinutes,\n  utcLabel,\n  zoneHour,\n} from \"./zone\"\n\n/** `list` 一行一个城市；`cards` 每个城市是一张带模拟表盘的卡片，两列排布。 */\ntype WorldClockVariant = \"list\" | \"cards\"\n\ntype WorldClockZone = {\n  /** IANA 时区名 */\n  timeZone: string\n  /** 城市名 */\n  label: React.ReactNode\n  /** 高亮这一项（例如“我所在的城市”） */\n  highlighted?: boolean\n}\n\ntype WorldClockSettings = {\n  locale: string\n  hour12: boolean\n  showSeconds: boolean\n  showZone: boolean\n  animated: boolean\n  variant: WorldClockVariant\n  labels: LocalTimeLabels\n}\n\nconst WorldClockContext = React.createContext<WorldClockSettings>({\n  locale: \"zh-CN\",\n  hour12: false,\n  showSeconds: true,\n  showZone: true,\n  animated: true,\n  variant: \"list\",\n  labels: DEFAULT_LABELS,\n})\n\ntype WorldClockProps = Omit<React.ComponentProps<\"div\">, \"title\"> & {\n  /** 用数据描述城市；与 `children` 二选一，同时传入时两者都会渲染 */\n  zones?: readonly WorldClockZone[]\n  /**\n   * 排布方式\n   * @default \"list\"\n   */\n  variant?: WorldClockVariant\n  /**\n   * 标题栏内容；传 `null` 隐藏整条标题栏\n   * @default \"WORLD CLOCK\"\n   */\n  title?: React.ReactNode\n  /**\n   * BCP 47 区域标识，决定时间与数字的格式\n   * @default \"zh-CN\"\n   */\n  locale?: string\n  /**\n   * 12 小时制（带上午/下午），默认 24 小时制\n   * @default false\n   */\n  hour12?: boolean\n  /**\n   * 显示秒\n   * @default true\n   */\n  showSeconds?: boolean\n  /**\n   * 在城市名下显示 UTC 偏移\n   * @default true\n   */\n  showZone?: boolean\n  /**\n   * 数字变化时播放轻微的下落淡入；系统开启“减少动态效果”时自动关闭\n   * @default true\n   */\n  animated?: boolean\n  /** 覆盖部分可见文案 */\n  labels?: Partial<LocalTimeLabels>\n}\n\n/** 世界时钟：多个时区同时显示，右侧标出与访客的时差、昼夜和日期差。 */\nfunction WorldClock({\n  zones,\n  variant = \"list\",\n  title,\n  locale = \"zh-CN\",\n  hour12 = false,\n  showSeconds = true,\n  showZone = true,\n  animated = true,\n  labels,\n  className,\n  children,\n  ...props\n}: WorldClockProps) {\n  const text = React.useMemo(() => ({ ...DEFAULT_LABELS, ...labels }), [labels])\n  const settings = React.useMemo(\n    () => ({\n      locale,\n      hour12,\n      showSeconds,\n      showZone,\n      animated,\n      variant,\n      labels: text,\n    }),\n    [locale, hour12, showSeconds, showZone, animated, variant, text],\n  )\n  const heading = title === undefined ? text.worldClock : title\n  return (\n    <WorldClockContext.Provider value={settings}>\n      <div\n        data-slot=\"world-clock\"\n        data-variant={variant}\n        role=\"group\"\n        aria-label={typeof heading === \"string\" ? heading : text.worldClock}\n        className={cn(\n          \"overflow-hidden rounded-lg border border-edge bg-background\",\n          className,\n        )}\n        {...props}\n      >\n        {heading === null ? null : (\n          <div\n            data-slot=\"world-clock-header\"\n            className=\"flex items-center justify-between gap-3 border-b border-edge px-3 py-2 font-mono text-[10px] tracking-[0.08em] text-muted-foreground\"\n          >\n            <span className=\"min-w-0 truncate\">{heading}</span>\n            <span className=\"flex shrink-0 items-center gap-1.5\">\n              <span\n                aria-hidden=\"true\"\n                className=\"size-1.5 rounded-full bg-primary motion-safe:animate-pulse\"\n              />\n              {text.live}\n            </span>\n          </div>\n        )}\n        <ul\n          data-slot=\"world-clock-list\"\n          className={cn(\n            variant === \"list\"\n              ? \"divide-y divide-edge\"\n              : // 单元格自带右、下边线，用负外边距把最外一圈挤出外框的裁剪区，避免与外框叠成双线\n                \"-mr-px -mb-px grid grid-cols-2 *:border-r *:border-b *:border-edge [&>li:last-child:nth-child(odd)]:col-span-2\",\n          )}\n        >\n          {zones?.map((zone) => (\n            <WorldClockItem\n              key={zone.timeZone}\n              timeZone={zone.timeZone}\n              label={zone.label}\n              highlighted={zone.highlighted}\n            />\n          ))}\n          {children}\n        </ul>\n      </div>\n    </WorldClockContext.Provider>\n  )\n}\n\ntype WorldClockItemProps = Omit<React.ComponentProps<\"li\">, \"children\"> & {\n  /** IANA 时区名 */\n  timeZone: string\n  /** 城市名 */\n  label: React.ReactNode\n  /**\n   * 高亮这一项\n   * @default false\n   */\n  highlighted?: boolean\n}\n\n/** 世界时钟中的一个城市；必须放在 `WorldClock` 里。 */\nfunction WorldClockItem({\n  timeZone,\n  label,\n  highlighted = false,\n  className,\n  ...props\n}: WorldClockItemProps) {\n  const { locale, hour12, showSeconds, showZone, animated, variant, labels } =\n    React.useContext(WorldClockContext)\n  const now = useNow(showSeconds ? 1000 : 60_000)\n  const date = now === null ? null : new Date(now)\n  const daylight = date ? isDaytime(zoneHour(timeZone, date)) : true\n  const days = date ? dayOffset(timeZone, date) : 0\n  const meta = date\n    ? [\n        days === 0 ? null : days > 0 ? labels.tomorrow : labels.yesterday,\n        describeDifference(hourDifference(timeZone, date), locale, labels),\n      ]\n        .filter(Boolean)\n        .join(\" · \")\n    : \"\"\n  const zone = [\n    timeZone.replaceAll(\"_\", \" \"),\n    date && showZone ? utcLabel(offsetMinutes(timeZone, date)) : null,\n  ]\n    .filter(Boolean)\n    .join(\" · \")\n  const time = date ? (\n    <time data-slot=\"world-clock-time\" dateTime={date.toISOString()}>\n      <TimeDigits\n        parts={clockParts(date, { locale, timeZone, hour12, showSeconds })}\n        animated={animated}\n      />\n    </time>\n  ) : (\n    <span data-slot=\"world-clock-time\">\n      {showSeconds ? \"--:--:--\" : \"--:--\"}\n    </span>\n  )\n  const mark = date ? (\n    <DayNightMark\n      daylight={daylight}\n      label={daylight ? labels.day : labels.night}\n      className=\"size-3.5\"\n    />\n  ) : null\n\n  return (\n    <li\n      data-slot=\"world-clock-item\"\n      data-state={date ? \"ready\" : \"pending\"}\n      data-highlighted={highlighted ? \"\" : undefined}\n      data-daylight={date ? (daylight ? \"day\" : \"night\") : undefined}\n      className={cn(\n        \"relative min-w-0 data-[highlighted]:bg-primary/5 data-[highlighted]:before:absolute data-[highlighted]:before:inset-y-0 data-[highlighted]:before:left-0 data-[highlighted]:before:w-0.5 data-[highlighted]:before:bg-primary\",\n        variant === \"list\"\n          ? \"flex items-center justify-between gap-3 px-3 py-2\"\n          : \"flex flex-col items-center gap-2 px-3 py-4 text-center\",\n        className,\n      )}\n      {...props}\n    >\n      {variant === \"cards\" ? (\n        <AnalogClock\n          timeZone={timeZone}\n          locale={locale}\n          label={typeof label === \"string\" ? label : labels.clock}\n          showSeconds={showSeconds}\n          className=\"size-20\"\n        />\n      ) : null}\n      <div\n        className={cn(\"min-w-0\", variant === \"list\" ? \"flex-1\" : \"max-w-full\")}\n      >\n        <p className=\"truncate text-sm leading-5 font-medium\">{label}</p>\n        <p className=\"truncate font-mono text-[10px] leading-4 text-muted-foreground\">\n          {zone}\n        </p>\n      </div>\n      <div\n        className={cn(\n          \"shrink-0 font-mono\",\n          variant === \"list\" ? \"text-right\" : \"text-center\",\n        )}\n      >\n        <p\n          className={cn(\n            \"flex items-center gap-1.5 text-sm leading-5 tabular-nums\",\n            variant === \"list\" ? \"justify-end\" : \"justify-center\",\n          )}\n        >\n          {mark}\n          {time}\n        </p>\n        <p className=\"min-h-4 text-[10px] leading-4 whitespace-nowrap text-muted-foreground\">\n          {meta}\n        </p>\n      </div>\n    </li>\n  )\n}\n\nexport { WorldClock, WorldClockItem }\nexport type {\n  WorldClockItemProps,\n  WorldClockProps,\n  WorldClockVariant,\n  WorldClockZone,\n}\n",
      "type": "registry:ui",
      "target": "@ui/local-time/world-clock.tsx"
    },
    {
      "path": "src/registry/ui/local-time/analog-clock.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { useAnimationFrame, useInView, useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useNow } from \"@/hooks/use-now\"\n\nimport { DEFAULT_LABELS } from \"./config\"\nimport { isDaytime, offsetMinutes, zoneHour } from \"./zone\"\n\ntype AnalogClockProps = Omit<React.ComponentProps<\"svg\">, \"children\"> & {\n  /** IANA 时区名，例如 `Asia/Shanghai` */\n  timeZone: string\n  /**\n   * BCP 47 区域标识，决定读屏标签里的时间格式\n   * @default \"zh-CN\"\n   */\n  locale?: string\n  /**\n   * 显示秒针\n   * @default true\n   */\n  showSeconds?: boolean\n  /**\n   * 指针每帧平滑转动；关闭后按秒跳格。系统开启“减少动态效果”时自动按秒跳格\n   * @default true\n   */\n  smooth?: boolean\n  /**\n   * 读屏名称，后面会拼上当前时间\n   * @default \"当地时间\"\n   */\n  label?: string\n}\n\n/** 时间折算成三根指针的角度（度）；`seconds` 带毫秒，用于平滑转动。 */\nfunction handAngles(zonedSeconds: number) {\n  const day = ((zonedSeconds % 86_400) + 86_400) % 86_400\n  return {\n    hour: (day % 43_200) / 120,\n    minute: (day % 3600) / 10,\n    second: (day % 60) * 6,\n  }\n}\n\n/**\n * SVG 模拟表盘。表盘随昼夜轻微变色，秒针用主题强调色。\n * 平滑模式下指针由 `requestAnimationFrame` 直接写入 transform，不经过 React 渲染；\n * 离开视口时暂停。\n */\nfunction AnalogClock({\n  timeZone,\n  locale = \"zh-CN\",\n  showSeconds = true,\n  smooth = true,\n  label = DEFAULT_LABELS.clock,\n  className,\n  ...props\n}: AnalogClockProps) {\n  const reduced = useReducedMotion()\n  const continuous = smooth && !reduced\n  const now = useNow(continuous ? 60_000 : 1000)\n  const ref = React.useRef<SVGSVGElement>(null)\n  const inView = useInView(ref)\n  const hands = React.useRef<\n    Record<\"hour\" | \"minute\" | \"second\", SVGGElement | null>\n  >({ hour: null, minute: null, second: null })\n  const date = now === null ? null : new Date(now)\n  const offset = date ? offsetMinutes(timeZone, date) : null\n  // 动画帧里不能再算时区：偏移随渲染（至少每分钟）更新到 ref，帧回调只做加法\n  const offsetRef = React.useRef<number | null>(null)\n  React.useEffect(() => {\n    offsetRef.current = offset\n  }, [offset])\n\n  useAnimationFrame(() => {\n    if (!continuous || !inView || offsetRef.current === null) return\n    const angles = handAngles(Date.now() / 1000 + offsetRef.current * 60)\n    hands.current.hour?.setAttribute(\n      \"transform\",\n      `rotate(${angles.hour} 50 50)`,\n    )\n    hands.current.minute?.setAttribute(\n      \"transform\",\n      `rotate(${angles.minute} 50 50)`,\n    )\n    hands.current.second?.setAttribute(\n      \"transform\",\n      `rotate(${angles.second} 50 50)`,\n    )\n  })\n\n  // 非平滑模式由 React 按秒写入角度；平滑模式只写初始值 0，之后交给动画帧\n  const stepped =\n    !continuous && date && offset !== null\n      ? handAngles(Math.floor(date.getTime() / 1000) + offset * 60)\n      : { hour: 0, minute: 0, second: 0 }\n  const daylight = date ? isDaytime(zoneHour(timeZone, date)) : true\n  const description = date\n    ? `${label} ${new Intl.DateTimeFormat(locale, {\n        timeZone,\n        hour: \"2-digit\",\n        minute: \"2-digit\",\n        hourCycle: \"h23\",\n      }).format(date)}`\n    : label\n\n  return (\n    <svg\n      ref={ref}\n      data-slot=\"analog-clock\"\n      data-state={date ? \"ready\" : \"pending\"}\n      data-daylight={daylight ? \"day\" : \"night\"}\n      viewBox=\"0 0 100 100\"\n      role=\"img\"\n      aria-label={description}\n      className={cn(\"size-28 text-foreground\", className)}\n      {...props}\n    >\n      <circle\n        cx=\"50\"\n        cy=\"50\"\n        r=\"48.5\"\n        className={cn(\n          \"stroke-edge\",\n          daylight ? \"fill-background\" : \"fill-foreground/5\",\n        )}\n        strokeWidth=\"1\"\n      />\n      {Array.from({ length: 12 }, (_, index) => (\n        <line\n          key={index}\n          x1=\"50\"\n          x2=\"50\"\n          y1={index % 3 === 0 ? 6 : 8}\n          y2={index % 3 === 0 ? 13 : 11.5}\n          transform={`rotate(${index * 30} 50 50)`}\n          className={\n            index % 3 === 0 ? \"stroke-foreground/70\" : \"stroke-foreground/30\"\n          }\n          strokeWidth={index % 3 === 0 ? 1.6 : 1}\n          strokeLinecap=\"round\"\n        />\n      ))}\n      <g\n        ref={(node) => void (hands.current.hour = node)}\n        transform={`rotate(${stepped.hour} 50 50)`}\n      >\n        <line\n          x1=\"50\"\n          y1=\"54\"\n          x2=\"50\"\n          y2=\"29\"\n          className=\"stroke-foreground\"\n          strokeWidth=\"3\"\n          strokeLinecap=\"round\"\n        />\n      </g>\n      <g\n        ref={(node) => void (hands.current.minute = node)}\n        transform={`rotate(${stepped.minute} 50 50)`}\n      >\n        <line\n          x1=\"50\"\n          y1=\"56\"\n          x2=\"50\"\n          y2=\"17\"\n          className=\"stroke-foreground\"\n          strokeWidth=\"2\"\n          strokeLinecap=\"round\"\n        />\n      </g>\n      {showSeconds ? (\n        <g\n          ref={(node) => void (hands.current.second = node)}\n          transform={`rotate(${stepped.second} 50 50)`}\n        >\n          <line\n            x1=\"50\"\n            y1=\"60\"\n            x2=\"50\"\n            y2=\"13\"\n            className=\"stroke-primary\"\n            strokeWidth=\"1\"\n            strokeLinecap=\"round\"\n          />\n        </g>\n      ) : null}\n      <circle cx=\"50\" cy=\"50\" r=\"2.4\" className=\"fill-primary\" />\n      <circle cx=\"50\" cy=\"50\" r=\"0.9\" className=\"fill-background\" />\n    </svg>\n  )\n}\n\nexport { AnalogClock }\nexport type { AnalogClockProps }\n",
      "type": "registry:ui",
      "target": "@ui/local-time/analog-clock.tsx"
    },
    {
      "path": "src/registry/ui/local-time/time-parts.tsx",
      "content": "\"use client\"\n\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { Moon, Sun } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/**\n * 一位数字；变化时旧数字向下淡出、新数字从上方落入。\n * 新旧两位叠在同一个 1ch 宽的格子里（grid 同格堆叠），过渡期间宽度不变，整行不会左右晃动。\n * 首次出现不播放，避免水合后整排数字同时闪一下。\n */\nfunction TimeDigit({ value, animated }: { value: string; animated: boolean }) {\n  if (!animated) return <>{value}</>\n  return (\n    <span\n      data-slot=\"time-digit\"\n      className=\"inline-grid w-[1ch] justify-items-center *:col-start-1 *:row-start-1\"\n    >\n      <AnimatePresence initial={false}>\n        <motion.span\n          key={value}\n          initial={{ opacity: 0, y: \"-0.35em\" }}\n          animate={{ opacity: 1, y: 0 }}\n          exit={{ opacity: 0, y: \"0.35em\" }}\n          transition={{ duration: 0.2, ease: \"easeOut\" }}\n        >\n          {value}\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  )\n}\n\n/**\n * 逐位渲染 `Intl.DateTimeFormat.formatToParts` 的结果。\n * 数字用 `tabular-nums` 等宽；分隔符与上午/下午等 dayPeriod 原样输出。\n */\nfunction TimeDigits({\n  parts,\n  animated = false,\n}: {\n  parts: readonly Intl.DateTimeFormatPart[]\n  /** 数字变化时是否播放过渡；系统开启“减少动态效果”时自动关闭 */\n  animated?: boolean\n}) {\n  const reduced = useReducedMotion()\n  const animate = animated && !reduced\n  return (\n    <span data-slot=\"time-digits\" className=\"tabular-nums\">\n      {parts.map((part, index) =>\n        part.type === \"hour\" ||\n        part.type === \"minute\" ||\n        part.type === \"second\" ? (\n          <span key={part.type} data-part={part.type}>\n            {Array.from(part.value).map((char, position) => (\n              <TimeDigit key={position} value={char} animated={animate} />\n            ))}\n          </span>\n        ) : (\n          <span\n            key={`${part.type}-${index}`}\n            data-part={part.type}\n            className={\n              part.type === \"dayPeriod\" ? \"mx-[0.2em] text-[0.8em]\" : undefined\n            }\n          >\n            {part.value}\n          </span>\n        ),\n      )}\n    </span>\n  )\n}\n\n/** 昼夜标记：白天是太阳（主题强调色），夜间是月亮（弱化色）；图标只是装饰，文字给读屏。 */\nfunction DayNightMark({\n  daylight,\n  label,\n  className,\n}: {\n  daylight: boolean\n  /** 读屏文字，如“白天” / “夜间” */\n  label: string\n  className?: string\n}) {\n  const Icon = daylight ? Sun : Moon\n  return (\n    <span\n      data-slot=\"day-night-mark\"\n      data-daylight={daylight ? \"day\" : \"night\"}\n      className={cn(\n        \"inline-flex shrink-0 align-[-0.125em]\",\n        daylight ? \"text-primary\" : \"text-muted-foreground\",\n        className,\n      )}\n    >\n      <Icon aria-hidden=\"true\" className=\"size-[1em]\" />\n      <span className=\"sr-only\">{label}</span>\n    </span>\n  )\n}\n\nexport { DayNightMark, TimeDigits }\n",
      "type": "registry:ui",
      "target": "@ui/local-time/time-parts.tsx"
    },
    {
      "path": "src/registry/ui/local-time/zone.ts",
      "content": "// 时区计算：全部基于 Intl，不引入时区库。Intl.DateTimeFormat 构造较慢，按参数缓存。\n\ntype ClockOptions = {\n  locale: string\n  timeZone: string\n  /** 12 小时制；默认 24 小时制 */\n  hour12?: boolean\n  showSeconds?: boolean\n}\n\nconst formatters = new Map<string, Intl.DateTimeFormat>()\n\nfunction formatter(\n  locale: string,\n  options: Intl.DateTimeFormatOptions,\n): Intl.DateTimeFormat {\n  const key = `${locale}|${JSON.stringify(options)}`\n  let cached = formatters.get(key)\n  if (!cached) {\n    cached = new Intl.DateTimeFormat(locale, options)\n    formatters.set(key, cached)\n  }\n  return cached\n}\n\n/** 把时间拆成可逐位渲染的片段（hour / minute / second / literal / dayPeriod）。 */\nfunction clockParts(date: Date, options: ClockOptions) {\n  return formatter(options.locale, {\n    timeZone: options.timeZone,\n    hour: \"2-digit\",\n    minute: \"2-digit\",\n    second: options.showSeconds === false ? undefined : \"2-digit\",\n    hourCycle: options.hour12 ? \"h12\" : \"h23\",\n  }).formatToParts(date)\n}\n\n/** 目标时区相对 UTC 的偏移分钟数，夏令时自动跟随。 */\nfunction offsetMinutes(timeZone: string, date: Date) {\n  const zone =\n    formatter(\"en-US\", { timeZone, timeZoneName: \"longOffset\" })\n      .formatToParts(date)\n      .find((part) => part.type === \"timeZoneName\")?.value ?? \"GMT\"\n  const match = zone.match(/GMT([+-])(\\d{2}):(\\d{2})/)\n  if (!match) return 0\n  const minutes = Number(match[2]) * 60 + Number(match[3])\n  return match[1] === \"+\" ? minutes : -minutes\n}\n\n/** 目标时区比访客时区快（正）或慢（负）几个小时。 */\nfunction hourDifference(timeZone: string, date: Date) {\n  return (offsetMinutes(timeZone, date) + date.getTimezoneOffset()) / 60\n}\n\n/** `UTC+8`、`UTC+5:30`、`UTC`。 */\nfunction utcLabel(minutes: number) {\n  if (minutes === 0) return \"UTC\"\n  const sign = minutes > 0 ? \"+\" : \"-\"\n  const abs = Math.abs(minutes)\n  const hours = Math.floor(abs / 60)\n  const rest = abs % 60\n  return `UTC${sign}${hours}${rest ? `:${String(rest).padStart(2, \"0\")}` : \"\"}`\n}\n\n/** 目标时区此刻的小时数（0–23）。 */\nfunction zoneHour(timeZone: string, date: Date) {\n  const hour = formatter(\"en-US\", {\n    timeZone,\n    hour: \"numeric\",\n    hourCycle: \"h23\",\n  }).format(date)\n  return Number(hour) % 24\n}\n\n/** 6:00–17:59 算白天；与时区所在半球和季节无关，只作视觉提示。 */\nfunction isDaytime(hour: number) {\n  return hour >= 6 && hour < 18\n}\n\n/** 目标时区的日期比访客本地日期晚一天（1）、早一天（-1）或同一天（0）。 */\nfunction dayOffset(timeZone: string, date: Date) {\n  const key = (parts: Intl.DateTimeFormatPart[]) => {\n    const value = (type: string) =>\n      parts.find((part) => part.type === type)?.value ?? \"\"\n    return `${value(\"year\")}-${value(\"month\")}-${value(\"day\")}`\n  }\n  const options = {\n    year: \"numeric\",\n    month: \"2-digit\",\n    day: \"2-digit\",\n  } as const\n  const target = key(\n    formatter(\"en-US\", { ...options, timeZone }).formatToParts(date),\n  )\n  const local = key(formatter(\"en-US\", options).formatToParts(date))\n  return target === local ? 0 : target > local ? 1 : -1\n}\n\n/** 时差的文字描述：与访客同一时间时返回 `same`，否则 `早/晚 N 小时`。 */\nfunction describeDifference(\n  difference: number,\n  locale: string,\n  text: { same: string; ahead: string; behind: string; hourUnit: string },\n) {\n  if (difference === 0) return text.same\n  const amount = new Intl.NumberFormat(locale, {\n    maximumFractionDigits: 2,\n  }).format(Math.abs(difference))\n  return `${difference > 0 ? text.ahead : text.behind} ${amount} ${text.hourUnit}`\n}\n\nexport {\n  clockParts,\n  dayOffset,\n  describeDifference,\n  hourDifference,\n  isDaytime,\n  offsetMinutes,\n  utcLabel,\n  zoneHour,\n}\nexport type { ClockOptions }\n",
      "type": "registry:ui",
      "target": "@ui/local-time/zone.ts"
    },
    {
      "path": "src/registry/ui/local-time/config.ts",
      "content": "/** 时钟类组件的可见文案；各组件通过 `labels` 部分覆盖。 */\ntype LocalTimeLabels = {\n  /** 挂载前，占位时间之后的说明 */\n  pending: string\n  /** 与访客同一时间 */\n  same: string\n  /** 目标时区更靠前时的前缀 */\n  ahead: string\n  /** 目标时区更靠后时的前缀 */\n  behind: string\n  /** 时差的单位 */\n  hourUnit: string\n  /** 白天标记的读屏文字 */\n  day: string\n  /** 夜间标记的读屏文字 */\n  night: string\n  /** 目标时区日期比访客早一天 */\n  yesterday: string\n  /** 与访客同一天 */\n  today: string\n  /** 目标时区日期比访客晚一天 */\n  tomorrow: string\n  /** 世界时钟的默认标题 */\n  worldClock: string\n  /** 世界时钟标题旁的“实时”标记 */\n  live: string\n  /** 模拟表盘的默认读屏名称 */\n  clock: string\n}\n\nconst DEFAULT_LABELS: LocalTimeLabels = {\n  pending: \"当地时间\",\n  same: \"与你同一时间\",\n  ahead: \"早\",\n  behind: \"晚\",\n  hourUnit: \"小时\",\n  day: \"白天\",\n  night: \"夜间\",\n  yesterday: \"昨天\",\n  today: \"今天\",\n  tomorrow: \"明天\",\n  worldClock: \"WORLD CLOCK\",\n  live: \"LIVE\",\n  clock: \"当地时间\",\n}\n\nexport { DEFAULT_LABELS }\nexport type { LocalTimeLabels }\n",
      "type": "registry:ui",
      "target": "@ui/local-time/config.ts"
    },
    {
      "path": "src/registry/hooks/use-now.ts",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\n// 全页面共用一个对齐整秒的定时器：多个时钟同时挂载也只有一个 setTimeout 在跑，\n// 并且所有实例的秒数在同一帧跳动。\nconst listeners = new Set<() => void>()\nlet timer: number | undefined\n\nfunction schedule() {\n  timer = window.setTimeout(\n    () => {\n      listeners.forEach((listener) => listener())\n      if (listeners.size) schedule()\n    },\n    1005 - (Date.now() % 1000),\n  ) // 多等 5ms，避免定时器提前触发时读到上一秒\n}\n\nfunction subscribe(listener: () => void) {\n  listeners.add(listener)\n  if (listeners.size === 1) schedule()\n  return () => {\n    listeners.delete(listener)\n    if (!listeners.size) window.clearTimeout(timer)\n  }\n}\n\n/**\n * 当前时间的毫秒时间戳，按 `step` 对齐（默认每秒）。\n * 服务端渲染与水合期间返回 `null`，水合完成后才给出真实时间，\n * 静态导出的 HTML 因此永远不会带着过期的时间。\n *\n * @param step 对齐粒度（毫秒）；快照不变时组件不会重新渲染，所以只显示分钟的组件传 `60_000` 即可\n */\nfunction useNow(step = 1000): number | null {\n  return React.useSyncExternalStore(\n    subscribe,\n    () => Math.floor(Date.now() / step) * step,\n    () => null,\n  )\n}\n\nexport { useNow }\n",
      "type": "registry:ui",
      "target": "hooks/use-now.ts"
    }
  ],
  "cssVars": {
    "theme": {
      "color-edge": "var(--edge)"
    },
    "light": {
      "edge": "color-mix(in oklch, var(--foreground) 12%, transparent)"
    },
    "dark": {
      "edge": "color-mix(in oklch, var(--foreground) 18%, transparent)"
    }
  },
  "type": "registry:ui"
}
