{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "availability-card",
  "type": "registry:ui",
  "title": "Availability Card",
  "description": "A hire-me card with availability status, live local time, tactile buttons and copy-email feedback.",
  "files": [
    {
      "path": "src/registry/ui/availability-card.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { useEffect, useRef, useState, useSyncExternalStore } from \"react\";\nimport Image from \"next/image\";\nimport { AlarmClock, Check, CirclePlus, Copy, Zap } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type AvailabilityCardImage = { src: string; alt: string };\n\nexport type AvailabilityCardProps = Omit<\n  React.ComponentProps<\"article\">,\n  \"title\"\n> & {\n  name: React.ReactNode;\n  role?: React.ReactNode;\n  avatar?: AvailabilityCardImage;\n  /** Shows the pulsing dot in `accent`; false shows a grey dot. */\n  available?: boolean;\n  statusLabel?: React.ReactNode;\n  /** IANA time zone, e.g. \"Asia/Kolkata\". Shows a live clock; omit to hide it. */\n  timeZone?: string;\n  /** Renders \"Hire Me\" as a link. Takes precedence over `onHire`. */\n  hireHref?: string;\n  onHire?: () => void;\n  hireLabel?: React.ReactNode;\n  /** Shows the copy button when set. */\n  email?: string;\n  onCopy?: (email: string) => void;\n  copyLabel?: React.ReactNode;\n  copiedLabel?: React.ReactNode;\n  /** Text on the accent strip under the card. */\n  note?: React.ReactNode;\n  noteIcon?: React.ReactNode;\n  /** Any CSS color. Used for the strip and the availability dot. */\n  accent?: string;\n};\n\nconst COPIED_RESET_MS = 2000;\n\nexport function AvailabilityCard({\n  name,\n  role,\n  avatar,\n  available = true,\n  statusLabel = available ? \"Available for work\" : \"Not available\",\n  timeZone,\n  hireHref,\n  onHire,\n  hireLabel = \"Hire Me\",\n  email,\n  onCopy,\n  copyLabel = \"Copy Email\",\n  copiedLabel = \"Copied!\",\n  note,\n  noteIcon = <Zap aria-hidden className=\"size-4\" />,\n  accent = \"#A8F25F\",\n  className,\n  style,\n  ...props\n}: AvailabilityCardProps) {\n  const time = useLocalTime(timeZone);\n  const { copied, copy } = useCopyToClipboard();\n\n  const handleCopy = async () => {\n    if (!email) return;\n    if (await copy(email)) onCopy?.(email);\n  };\n\n  const hireContent = (\n    <>\n      <CirclePlus aria-hidden className=\"size-5\" />\n      {hireLabel}\n    </>\n  );\n\n  return (\n    <article\n      data-slot=\"availability-card\"\n      style={\n        { \"--availability-accent\": accent, ...style } as React.CSSProperties\n      }\n      className={cn(\n        \"w-full max-w-xl rounded-[2.5rem] bg-blue-600 shadow-xl shadow-black/10 p-0.5\",\n        className,\n      )}\n      {...props}\n    >\n      <div className=\"rounded-[2.5rem] bg-neutral-900 p-6 text-white shadow-[0_12px_24px_-12px_rgba(0,0,0,0.5)] sm:p-8\">\n        <div className=\"flex items-center justify-between gap-4 text-sm text-white/55 sm:text-base\">\n          <p className=\"flex items-center gap-2.5\">\n            <StatusDot available={available} />\n            {statusLabel}\n          </p>\n          {timeZone && (\n            <p className=\"flex items-center gap-1.5 tabular-nums\">\n              <AlarmClock aria-hidden className=\"size-4\" />\n              {/* Empty until mounted: the server can't know the visitor's clock. */}\n              <span className=\"sr-only\">Local time </span>\n              <span className=\"min-w-[4.5ch]\">{time ?? \" \"}</span>\n            </p>\n          )}\n        </div>\n\n        <div className=\"mt-7 flex items-center gap-4 sm:gap-5\">\n          {avatar && (\n            <Image\n              src={avatar.src}\n              alt={avatar.alt}\n              width={80}\n              height={80}\n              className=\"size-16 shrink-0 rounded-full object-cover sm:size-20\"\n            />\n          )}\n          <div className=\"min-w-0\">\n            <h3 className=\"truncate text-2xl leading-tight tracking-[-0.01em] sm:text-[2rem]\">\n              {name}\n            </h3>\n            {role && (\n              <p className=\"truncate text-base text-white/50 sm:text-lg\">\n                {role}\n              </p>\n            )}\n          </div>\n        </div>\n\n        {(hireHref || onHire || email) && (\n          <div className=\"mt-8 grid grid-cols-1 gap-3 min-[420px]:grid-cols-2 sm:gap-4\">\n            {hireHref ? (\n              <a\n                href={hireHref}\n                className={cn(\n                  buttonClass,\n                  \"bg-neutral-600 hover:bg-neutral-500\",\n                )}\n              >\n                {hireContent}\n              </a>\n            ) : onHire ? (\n              <button\n                type=\"button\"\n                onClick={onHire}\n                className={cn(\n                  buttonClass,\n                  \"bg-neutral-600 hover:bg-neutral-500\",\n                )}\n              >\n                {hireContent}\n              </button>\n            ) : null}\n\n            {email && (\n              <button\n                type=\"button\"\n                onClick={handleCopy}\n                className={cn(\n                  buttonClass,\n                  \"bg-neutral-800 hover:bg-neutral-700\",\n                )}\n              >\n                {/* Both states share one cell, so the button never changes size. */}\n                <span className=\"grid place-items-center\">\n                  <SwapLabel visible={!copied}>\n                    <Copy aria-hidden className=\"size-5\" />\n                    {copyLabel}\n                  </SwapLabel>\n                  <SwapLabel visible={copied}>\n                    <Check aria-hidden className=\"size-5\" />\n                    {copiedLabel}\n                  </SwapLabel>\n                </span>\n              </button>\n            )}\n          </div>\n        )}\n        <span role=\"status\" className=\"sr-only\">\n          {copied ? \"Email copied to clipboard\" : \"\"}\n        </span>\n      </div>\n\n      {note && (\n        <p className=\"flex items-center justify-center gap-2 px-6 pt-4 pb-5 text-center text-base font-medium text-foreground\">\n          {noteIcon}\n          {note}\n        </p>\n      )}\n    </article>\n  );\n}\n\n// Raised, tactile buttons: a top highlight, a soft drop shadow and a quick press dip.\nconst buttonClass = cn(\n  \"inline-flex h-14 cursor-pointer items-center justify-center gap-2.5 rounded-2xl px-5 text-lg font-medium text-white outline-none\",\n  \"shadow-[inset_0_1px_0_rgb(255_255_255/0.08),0_10px_20px_-8px_rgb(0_0_0/0.6)] ring-1 ring-white/5\",\n  \"transition-[background-color,transform] duration-[160ms] ease-[cubic-bezier(0.23,1,0.32,1)]\",\n  \"active:scale-[0.97] motion-reduce:active:scale-100\",\n  \"focus-visible:ring-2 focus-visible:ring-white/70\",\n);\n\nfunction SwapLabel({\n  visible,\n  children,\n}: {\n  visible: boolean;\n  children: React.ReactNode;\n}) {\n  return (\n    <span\n      aria-hidden={!visible}\n      className={cn(\n        \"inline-flex items-center gap-2.5 [grid-area:1/1]\",\n        \"transition-[opacity,filter] duration-150 ease-[cubic-bezier(0.23,1,0.32,1)]\",\n        visible\n          ? \"opacity-100\"\n          : \"opacity-0 blur-[2px] motion-reduce:blur-none\",\n      )}\n    >\n      {children}\n    </span>\n  );\n}\n\nfunction StatusDot({ available }: { available: boolean }) {\n  return (\n    <span aria-hidden className=\"relative flex size-2\">\n      {available && (\n        <span className=\"absolute inset-0 animate-ping rounded-full bg-[var(--availability-accent)] opacity-60 [animation-duration:2s] motion-reduce:hidden\" />\n      )}\n      <span\n        className={cn(\n          \"relative size-2 rounded-full\",\n          available ? \"bg-[var(--availability-accent)]\" : \"bg-neutral-500\",\n        )}\n      />\n    </span>\n  );\n}\n\n// Formats \"8:30PM\" in a time zone, re-rendering on each minute boundary.\nfunction useLocalTime(timeZone: string | undefined) {\n  return useSyncExternalStore(\n    subscribeToMinutes,\n    () => (timeZone ? formatTime(timeZone) : null),\n    () => null,\n  );\n}\n\nfunction subscribeToMinutes(onTick: () => void) {\n  let interval: ReturnType<typeof setInterval> | undefined;\n  const timeout = setTimeout(\n    () => {\n      onTick();\n      interval = setInterval(onTick, 60_000);\n    },\n    60_000 - (Date.now() % 60_000),\n  );\n  return () => {\n    clearTimeout(timeout);\n    clearInterval(interval);\n  };\n}\n\nconst formatters = new Map<string, Intl.DateTimeFormat>();\n\nfunction formatTime(timeZone: string) {\n  let formatter = formatters.get(timeZone);\n  if (!formatter) {\n    formatter = new Intl.DateTimeFormat(\"en-US\", {\n      hour: \"numeric\",\n      minute: \"2-digit\",\n      timeZone,\n    });\n    formatters.set(timeZone, formatter);\n  }\n  return formatter\n    .formatToParts(new Date())\n    .filter((part) => part.type !== \"literal\" || part.value === \":\")\n    .map((part) => part.value)\n    .join(\"\");\n}\n\nfunction useCopyToClipboard() {\n  const [copied, setCopied] = useState(false);\n  const timeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(\n    undefined,\n  );\n\n  useEffect(() => () => clearTimeout(timeoutRef.current), []);\n\n  const copy = async (text: string) => {\n    try {\n      await navigator.clipboard.writeText(text);\n    } catch {\n      return false;\n    }\n    setCopied(true);\n    clearTimeout(timeoutRef.current);\n    timeoutRef.current = setTimeout(() => setCopied(false), COPIED_RESET_MS);\n    return true;\n  };\n\n  return { copied, copy };\n}\n"
    }
  ],
  "dependencies": [
    "lucide-react"
  ]
}
