{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "opening-hours",
  "title": "Opening hours",
  "description": "Opening hours or availability per weekday, with more than one range a day.",
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/manniche/opening-hours/opening-hours.tsx",
      "content": "// Based on Watermelon UI's “Slot picker” (MIT, © 2026 Watermelon Platform Contributors,\n// github.com/WatermelonCorp/watermelon-platform). Rewritten with native time inputs and a real switch.\nimport { Plus, X } from 'lucide-react'\nimport { AnimatePresence, LayoutGroup, MotionConfig, motion } from 'motion/react'\nimport { useId, useState } from 'react'\nimport { Switch } from '@/registry/manniche/switch/switch'\nimport { cn } from '@/lib/utils'\n\nexport type TimeRange = { id: string; from: string; to: string }\nexport type OpeningDay = { id: string; label: string; open: boolean; ranges: TimeRange[] }\n\nexport type OpeningHoursProps = {\n  /** Controlled week. Times are \"HH:MM\", as a time input gives them. */\n  value?: OpeningDay[]\n  defaultValue?: OpeningDay[]\n  onChange?: (days: OpeningDay[]) => void\n  labels?: { from?: string; to?: string; add?: string; remove?: string }\n  className?: string\n}\n\n/** Opening hours or availability per weekday, with more than one range a day if needed. */\nexport function OpeningHours({ value, defaultValue = [], onChange, labels = {}, className }: OpeningHoursProps) {\n  const [own, setOwn] = useState(defaultValue)\n  const days = value ?? own\n  const group = useId()\n  const { from = 'From', to = 'To', add = 'Add hours', remove = 'Remove hours' } = labels\n\n  const set = (next: OpeningDay[]) => {\n    setOwn(next)\n    onChange?.(next)\n  }\n  const patch = (id: string, f: (d: OpeningDay) => OpeningDay) => set(days.map((d) => (d.id === id ? f(d) : d)))\n  const range = (from: string, to: string): TimeRange => ({ id: crypto.randomUUID(), from, to })\n\n  const time = 'h-10 rounded-xl border bg-background px-2 text-sm tabular-nums outline-none focus-visible:ring-2 focus-visible:ring-ring'\n\n  return (\n    <MotionConfig reducedMotion=\"user\" transition={{ type: 'spring', stiffness: 420, damping: 34 }}>\n      <LayoutGroup id={group}>\n        <div className={cn('flex w-full max-w-sm flex-col gap-2', className)}>\n          {days.map((day) => (\n            <motion.section\n              key={day.id}\n              layout\n              aria-label={day.label}\n              style={{ borderRadius: 22 }}\n              className={cn('overflow-hidden border transition-colors duration-300', day.open ? 'bg-card shadow-sm' : 'border-transparent bg-muted')}\n            >\n              <motion.div layout=\"position\" className=\"flex h-14 items-center justify-between px-4\">\n                <span className=\"font-medium\">{day.label}</span>\n                <Switch\n                  aria-label={day.label}\n                  checked={day.open}\n                  onChange={(open) => patch(day.id, (d) => ({ ...d, open, ranges: open && d.ranges.length === 0 ? [range('09:00', '17:00')] : d.ranges }))}\n                />\n              </motion.div>\n              <AnimatePresence initial={false}>\n                {day.open && (\n                  <motion.div initial={{ height: 0, opacity: 0 }} animate={{ height: 'auto', opacity: 1 }} exit={{ height: 0, opacity: 0 }}>\n                    <ul className=\"space-y-2 px-4 pb-2\">\n                      <AnimatePresence initial={false}>\n                        {day.ranges.map((r) => (\n                          <motion.li key={r.id} layout initial={{ opacity: 0, y: -6 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0 }} className=\"flex items-center gap-2\">\n                            <input type=\"time\" aria-label={`${day.label}, ${from}`} value={r.from} className={time}\n                              onChange={(e) => patch(day.id, (d) => ({ ...d, ranges: d.ranges.map((x) => (x.id === r.id ? { ...x, from: e.target.value } : x)) }))} />\n                            <span className=\"text-muted-foreground\" aria-hidden>–</span>\n                            <input type=\"time\" aria-label={`${day.label}, ${to}`} value={r.to} className={time}\n                              onChange={(e) => patch(day.id, (d) => ({ ...d, ranges: d.ranges.map((x) => (x.id === r.id ? { ...x, to: e.target.value } : x)) }))} />\n                            <button\n                              type=\"button\"\n                              aria-label={remove}\n                              onClick={() => patch(day.id, (d) => {\n                                const ranges = d.ranges.filter((x) => x.id !== r.id)\n                                return { ...d, ranges, open: ranges.length > 0 }\n                              })}\n                              className=\"ml-auto grid size-10 place-items-center rounded-xl text-muted-foreground transition-colors duration-150 hover:bg-muted hover:text-foreground\"\n                            >\n                              <X className=\"size-4\" aria-hidden />\n                            </button>\n                          </motion.li>\n                        ))}\n                      </AnimatePresence>\n                    </ul>\n                    <button\n                      type=\"button\"\n                      onClick={() => patch(day.id, (d) => ({ ...d, ranges: [...d.ranges, range(d.ranges.at(-1)?.to ?? '09:00', '18:00')] }))}\n                      className=\"mx-4 mb-3 inline-flex min-h-10 items-center gap-1.5 rounded-xl px-2 text-sm text-muted-foreground transition-colors duration-150 hover:bg-muted hover:text-foreground\"\n                    >\n                      <Plus className=\"size-4\" aria-hidden />\n                      {add}\n                    </button>\n                  </motion.div>\n                )}\n              </AnimatePresence>\n            </motion.section>\n          ))}\n        </div>\n      </LayoutGroup>\n    </MotionConfig>\n  )\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/manniche/switch/switch.tsx",
      "content": "import { MotionConfig, motion } from 'motion/react'\nimport { useId, useState } from 'react'\nimport { cn } from '@/lib/utils'\n\nexport type SwitchProps = {\n  /** Controlled state. */\n  checked?: boolean\n  defaultChecked?: boolean\n  onChange?: (checked: boolean) => void\n  /** Visible label. Leave it out and pass `aria-label` instead. */\n  label?: string\n  'aria-label'?: string\n  disabled?: boolean\n  className?: string\n}\n\n/** A switch whose knob stretches as you press it and springs across, like the one on a phone. */\nexport function Switch({ checked, defaultChecked = false, onChange, label, disabled, className, ...aria }: SwitchProps) {\n  const [own, setOwn] = useState(defaultChecked)\n  const on = checked ?? own\n  const id = useId()\n\n  const flip = () => {\n    setOwn(!on)\n    onChange?.(!on)\n  }\n\n  return (\n    <MotionConfig reducedMotion=\"user\">\n      <span className={cn('inline-flex items-center gap-3', className)}>\n        <motion.button\n          id={id}\n          type=\"button\"\n          role=\"switch\"\n          aria-checked={on}\n          aria-label={aria['aria-label']}\n          disabled={disabled}\n          onClick={flip}\n          whileTap=\"pressed\"\n          className={cn(\n            'relative flex h-8 w-[52px] shrink-0 items-center rounded-full p-[3px] transition-colors duration-200 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-40',\n            on ? 'justify-end bg-emerald-500' : 'justify-start bg-muted',\n          )}\n        >\n          <motion.span\n            layout\n            // Pressed, the knob stretches towards the side it is about to move to.\n            variants={{ pressed: { scaleX: 1.2 } }}\n            style={{ originX: on ? 1 : 0 }}\n            transition={{ type: 'spring', stiffness: 520, damping: 46 }}\n            className=\"block h-[26px] w-[26px] rounded-full bg-white shadow-[0_2px_6px_rgb(0_0_0/0.18),0_0_0_0.5px_rgb(0_0_0/0.06)]\"\n          />\n        </motion.button>\n        {label && (\n          <label htmlFor={id} className=\"text-sm font-medium select-none\">\n            {label}\n          </label>\n        )}\n      </span>\n    </MotionConfig>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "meta": {
    "credit": "Adapted from Watermelon UI (MIT)."
  },
  "categories": [
    "widgets"
  ],
  "type": "registry:component"
}