{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "lock-screen",
  "title": "Lock Screen",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A full-screen lock screen in two flavors: macOS (live clock, avatar, password field that shakes when wrong) and iOS (clock, notification stack, flashlight and camera, swipe up to unlock). Hydration-safe clock.",
  "dependencies": [
    "motion@^12",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/motion-presets.json",
    "https://ui.ballmac.com/r/i18n.json",
    "https://ui.ballmac.com/r/media.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/lock-screen.tsx",
      "content": "// Ballmac UI: Lock Screen. https://ui.ballmac.com/components/lock-screen\n\"use client\"\n\nimport * as React from \"react\"\nimport { ArrowRight, Camera, Flashlight, Lock } from \"lucide-react\"\nimport { AnimatePresence, motion, useAnimationControls } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useReducedMotionSafe } from \"@/lib/ballmac/motion\"\nimport { useMessages } from \"@/lib/ballmac/i18n\"\nimport { Media, isMediaImage, type MediaSource } from \"@/components/ballmac/media\"\n\ntype LockScreenProps = Omit<React.ComponentProps<\"section\">, \"onSubmit\"> & {\n  /** \"mac\" shows a password field under the user's avatar; \"ios\" shows notifications and a swipe-up unlock. */\n  variant?: \"mac\" | \"ios\"\n  /** Controlled state. While true the lock screen covers its positioned parent. */\n  locked?: boolean\n  /** Initial state when uncontrolled. */\n  defaultLocked?: boolean\n  /** Called with the new state when the screen unlocks. */\n  onLockedChange?: (locked: boolean) => void\n  /** Time to show, such as \"9:41\". Omit for a live clock. */\n  time?: string\n  /** Date to show, such as \"Thursday, October 1\". Omit for the live date. */\n  date?: string\n  /** Name under the avatar (mac). */\n  name?: string\n  /** Replaces the default avatar (mac): an image, icon or initials. */\n  avatar?: React.ReactNode\n  /** The correct password (mac). Without it, any entry unlocks. */\n  password?: string\n  /** Extra check on the entered password (mac). Return false to reject it. */\n  onUnlock?: (password: string) => boolean | void\n  /** Hint under the field (mac). */\n  hint?: string\n  /** Replaces the default wallpaper. An image URL, an object with alt text and a dark-mode file, or an element that fills it; text stays white, so keep it dark enough. */\n  wallpaper?: MediaSource\n  /** Notifications on iOS, or controls along the bottom on mac. */\n  children?: React.ReactNode\n}\n\nfunction useClock(fixedTime?: string, fixedDate?: string) {\n  const [now, setNow] = React.useState<Date | null>(null)\n  React.useEffect(() => {\n    if (fixedTime && fixedDate) return\n    let timer: ReturnType<typeof setTimeout>\n    const tick = () => {\n      const d = new Date()\n      setNow(d)\n      timer = setTimeout(tick, 60000 - (d.getSeconds() * 1000 + d.getMilliseconds()) + 50)\n    }\n    tick()\n    return () => clearTimeout(timer)\n  }, [fixedTime, fixedDate])\n  return { now }\n}\n\nfunction format(now: Date | null, variant: \"mac\" | \"ios\") {\n  // Until mounted, render a fixed value so server and browser agree.\n  if (!now) return { time: \"9:41\", date: variant === \"ios\" ? \"Thursday, October 1\" : \"Thu Oct 1\" }\n  const time = now.toLocaleTimeString(\"en-US\", { hour: \"numeric\", minute: \"2-digit\", hour12: true }).replace(/\\s?[AP]M$/i, \"\")\n  const date =\n    variant === \"ios\"\n      ? now.toLocaleDateString(\"en-US\", { weekday: \"long\", month: \"long\", day: \"numeric\" })\n      : now.toLocaleDateString(\"en-US\", { weekday: \"short\", month: \"short\", day: \"numeric\" }).replace(\",\", \"\")\n  return { time, date }\n}\n\nfunction DefaultWallpaper() {\n  return (\n    <div aria-hidden=\"true\" data-slot=\"lock-screen-wallpaper\" className=\"absolute inset-0 overflow-hidden bg-[oklch(0.3_0.1_282)]\">\n      <div className=\"absolute -top-1/4 -start-1/4 size-[85%] rounded-full bg-[oklch(0.5_0.2_305)] opacity-70 blur-[70px]\" />\n      <div className=\"absolute -end-1/4 bottom-[-20%] size-[90%] rounded-full bg-[oklch(0.5_0.17_28)] opacity-70 blur-[80px]\" />\n      <div className=\"absolute top-1/3 start-1/3 size-[55%] rounded-full bg-[oklch(0.45_0.15_235)] opacity-60 blur-[70px]\" />\n    </div>\n  )\n}\n\n/**\n * A full-screen lock screen in two flavors: macOS (big clock, avatar, password field that shakes when wrong) and\n * iOS (date, huge clock, notification stack, flashlight and camera buttons, swipe up to unlock).\n * It covers its positioned parent and slides or fades away when unlocked. The clock is live and hydration-safe.\n */\nfunction LockScreen({\n  variant = \"mac\",\n  locked: lockedProp,\n  defaultLocked = true,\n  onLockedChange,\n  time,\n  date,\n  name = \"Alex Morgan\",\n  avatar,\n  password,\n  onUnlock,\n  hint,\n  wallpaper,\n  children,\n  className,\n  ...props\n}: LockScreenProps) {\n  const msg = useMessages()\n  hint ??= msg(\"lock-screen.hint\", \"Touch ID or enter password\")\n  const reduce = useReducedMotionSafe()\n  const uid = React.useId()\n  const [lockedState, setLockedState] = React.useState(defaultLocked)\n  const locked = lockedProp ?? lockedState\n  const { now } = useClock(time, date)\n  const shown = format(now, variant)\n  const clock = time ?? shown.time\n  const day = date ?? shown.date\n  const [value, setValue] = React.useState(\"\")\n  const [failed, setFailed] = React.useState(false)\n  const shake = useAnimationControls()\n  const inputRef = React.useRef<HTMLInputElement>(null)\n\n  function unlock() {\n    setLockedState(false)\n    setValue(\"\")\n    setFailed(false)\n    onLockedChange?.(false)\n  }\n\n  // A lock screen that comes back starts clean.\n  React.useEffect(() => {\n    if (locked) {\n      setValue(\"\")\n      setFailed(false)\n    }\n  }, [locked])\n\n  async function submit(e: React.FormEvent) {\n    e.preventDefault()\n    const ok = (password === undefined || value === password) && onUnlock?.(value) !== false\n    if (ok) return unlock()\n    setFailed(true)\n    setValue(\"\")\n    inputRef.current?.focus()\n    if (!reduce) await shake.start({ x: [0, -12, 11, -9, 7, -4, 2, 0], transition: { duration: 0.45, ease: \"easeOut\" } })\n  }\n\n  const exit = reduce\n    ? { opacity: 0 }\n    : variant === \"ios\"\n      ? { y: \"-100%\", transition: { duration: 0.45, ease: [0.32, 0.72, 0, 1] as const } }\n      : { opacity: 0, scale: 1.08, filter: \"blur(12px)\", transition: { duration: 0.4, ease: [0.22, 1, 0.36, 1] as const } }\n\n  return (\n    <AnimatePresence>\n      {locked && (\n        <motion.section\n          key=\"lock\"\n          data-slot=\"lock-screen\"\n          data-variant={variant}\n          aria-label={msg(\"lock-screen.lockScreen\", \"Lock screen\")}\n          className={cn(\"@container absolute inset-0 z-50 flex select-none flex-col overflow-hidden text-white\", className)}\n          initial={false}\n          exit={exit}\n          {...(variant === \"ios\" && !reduce\n            ? {\n                drag: \"y\" as const,\n                dragConstraints: { top: 0, bottom: 0 },\n                dragElastic: { top: 0.6, bottom: 0 },\n                onDragEnd: (_: unknown, info: { offset: { y: number }; velocity: { y: number } }) => {\n                  if (info.offset.y < -110 || info.velocity.y < -600) unlock()\n                },\n              }\n            : {})}\n          {...(props as object)}\n        >\n          {isMediaImage(wallpaper) ? <Media media={wallpaper} alt=\"\" fill priority className=\"absolute inset-0\" /> : (wallpaper ?? <DefaultWallpaper />)}\n          <div aria-hidden=\"true\" className=\"absolute inset-0 bg-black/20\" />\n\n          {variant === \"ios\" ? (\n            <div className=\"relative flex min-h-0 flex-1 flex-col items-center px-[6cqw] pt-[14cqw] pb-[9cqw]\">\n              <p className=\"text-[clamp(0.8rem,4.6cqw,1.35rem)] font-medium text-white/90 [text-shadow:0_1px_8px_rgb(0_0_0/0.3)]\">{day}</p>\n              <p className=\"-mt-[1cqw] bg-gradient-to-b from-white to-white/70 bg-clip-text text-[clamp(4rem,23cqw,9rem)] leading-[1.05] font-semibold tracking-tight tabular-nums text-transparent\">\n                {clock}\n              </p>\n              <div className=\"mt-[6cqw] flex min-h-0 w-full max-w-[34rem] flex-1 flex-col gap-[2cqw] overflow-hidden\">{children}</div>\n              <div className=\"mt-[3cqw] flex w-full max-w-[34rem] items-center justify-between px-[2cqw]\">\n                {[\n                  [Flashlight, \"Flashlight\"],\n                  [Camera, \"Camera\"],\n                ].map(([Glyph, label]) => {\n                  const Icon = Glyph as typeof Flashlight\n                  return (\n                    <button\n                      key={label as string}\n                      type=\"button\"\n                      aria-label={label as string}\n                      className=\"flex size-[clamp(2.75rem,12.5cqw,4rem)] items-center justify-center rounded-full bg-black/35 text-white outline-none backdrop-blur-xl transition-colors hover:bg-black/45 focus-visible:ring-[3px] focus-visible:ring-white/70 [&_svg]:size-[45%]\"\n                    >\n                      <Icon aria-hidden=\"true\" />\n                    </button>\n                  )\n                })}\n              </div>\n              <button\n                type=\"button\"\n                onClick={unlock}\n                className=\"mt-[3cqw] rounded-full px-4 py-2 text-[clamp(0.75rem,3.4cqw,0.95rem)] font-medium text-white/85 outline-none focus-visible:ring-[3px] focus-visible:ring-white/70\"\n              >\n                <span aria-hidden=\"true\" className=\"mx-auto mb-2 block h-1 w-28 rounded-full bg-white/85\" />\n                {reduce ? \"Unlock\" : \"Swipe up to unlock\"}\n              </button>\n            </div>\n          ) : (\n            <div className=\"relative flex min-h-0 flex-1 flex-col items-center px-6 pt-[clamp(2rem,6vw,4rem)] pb-6\">\n              <p className=\"text-[clamp(1rem,2.4cqw,1.5rem)] font-medium text-white/90 [text-shadow:0_1px_8px_rgb(0_0_0/0.3)]\">{day}</p>\n              <p className=\"bg-gradient-to-b from-white to-white/75 bg-clip-text text-[clamp(4.5rem,13cqw,9rem)] leading-none font-bold tracking-tight tabular-nums text-transparent\">\n                {clock}\n              </p>\n              <div className=\"mt-auto flex w-full flex-col items-center\">\n                <div className=\"flex size-[4.75rem] items-center justify-center overflow-hidden rounded-full bg-white/20 text-3xl font-semibold shadow-[0_2px_12px_rgb(0_0_0/0.35)] ring-1 ring-white/30 backdrop-blur-xl [&>img]:size-full [&>img]:object-cover [&>svg]:size-[55%]\">\n                  {avatar ?? <span aria-hidden=\"true\">{name.split(/\\s+/).map((p) => p[0]).slice(0, 2).join(\"\").toUpperCase()}</span>}\n                </div>\n                <p className=\"mt-3 text-lg font-semibold [text-shadow:0_1px_6px_rgb(0_0_0/0.35)]\">{name}</p>\n                <motion.form animate={shake} onSubmit={submit} className=\"relative mt-3 w-full max-w-[15rem]\" noValidate>\n                  <label htmlFor={`${uid}-pw`} className=\"sr-only\">\n                    {msg(\"lock-screen.passwordFor\", \"Password for {name}\", { name })}\n                  </label>\n                  <input\n                    ref={inputRef}\n                    id={`${uid}-pw`}\n                    type=\"password\"\n                    autoComplete=\"current-password\"\n                    value={value}\n                    onChange={(e) => setValue(e.target.value)}\n                    placeholder={msg(\"lock-screen.enterPassword\", \"Enter Password\")}\n                    aria-invalid={failed || undefined}\n                    aria-describedby={`${uid}-hint`}\n                    className=\"h-9 w-full rounded-full border border-white/25 bg-white/20 pe-10 ps-4 text-[15px] text-white shadow-[inset_0_1px_0_rgb(255_255_255/0.2)] outline-none backdrop-blur-xl placeholder:text-white/80 focus-visible:border-white/60 focus-visible:ring-[3px] focus-visible:ring-white/40\"\n                  />\n                  <button\n                    type=\"submit\"\n                    aria-label={msg(\"lock-screen.unlock\", \"Unlock\")}\n                    className=\"absolute top-1/2 end-1.5 flex size-6 -translate-y-1/2 items-center justify-center rounded-full bg-white/30 text-white outline-none transition-colors hover:bg-white/45 focus-visible:ring-2 focus-visible:ring-white\"\n                  >\n                    <ArrowRight aria-hidden=\"true\" className=\"size-3.5 rtl:rotate-180\" strokeWidth={2.75} />\n                  </button>\n                </motion.form>\n                <p id={`${uid}-hint`} role={failed ? \"alert\" : undefined} className=\"mt-2 flex items-center gap-1.5 text-xs font-medium text-white/85 [text-shadow:0_1px_4px_rgb(0_0_0/0.4)]\">\n                  {failed ? (\n                    \"Incorrect password. Try again.\"\n                  ) : (\n                    <>\n                      <Lock aria-hidden=\"true\" className=\"size-3\" />\n                      {hint}\n                    </>\n                  )}\n                </p>\n                {children && <div className=\"mt-6 flex items-center justify-center gap-6\">{children}</div>}\n              </div>\n            </div>\n          )}\n        </motion.section>\n      )}\n    </AnimatePresence>\n  )\n}\n\nexport { LockScreen, type LockScreenProps }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/lock-screen.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "lock-screen",
      "login",
      "macos",
      "ios",
      "clock",
      "password",
      "wallpaper"
    ],
    "version": "1.0.0",
    "updated": "2026-10-01",
    "ai": {
      "summary": "<LockScreen variant=\"mac\" password=\"hello\" /> fills its positioned parent until unlocked. variant=\"ios\" takes notifications as children and unlocks by swiping up or pressing the button. Control with locked / onLockedChange.",
      "whenToUse": [
        "Mac or phone UI demos with a login moment",
        "Idle or session-timeout screens"
      ],
      "whenNotToUse": [
        "Real authentication: the password prop is for demos, check credentials on a server"
      ],
      "composesWith": [
        "phone-frame",
        "notification-stack",
        "mac-window",
        "menu-bar"
      ],
      "a11y": [
        {
          "keys": "Tab",
          "action": "Reaches the password field and the Unlock button"
        },
        {
          "keys": "Enter",
          "action": "Submits the password"
        },
        {
          "keys": "Wrong password",
          "action": "Announced as an alert and the field shakes (no shake with reduced motion)"
        },
        {
          "keys": "Reduced motion",
          "action": "Fade instead of slide, blur and scale; iOS shows an Unlock button"
        }
      ],
      "customization": [
        "variant: mac | ios",
        "password, onUnlock, hint, name, avatar",
        "time and date for a fixed clock (omit for live)",
        "wallpaper replaces the default",
        "locked / defaultLocked / onLockedChange"
      ]
    },
    "examples": [
      "lock-screen-demo",
      "lock-screen-ios"
    ],
    "url": "https://ui.ballmac.com/components/lock-screen"
  },
  "categories": [
    "macos"
  ],
  "type": "registry:ui"
}