{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "calendar-motion",
  "title": "Motion calendar",
  "description": "Styled calendar: the Motion set. 5 decorative calendar variations (SlideCalendar, FadeCalendar, PopCalendar, SpringCalendar, MorphCalendar) on the ai2 token system, powered by framer-motion, reduced-motion aware and sized sm to xl. Part of the free styled layer.",
  "dependencies": [
    "motion@^12.42.2",
    "lucide-react@^1.23.0"
  ],
  "registryDependencies": [
    "@ai2/tokens"
  ],
  "files": [
    {
      "path": "registry/ai2/styled/calendar-motion.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport type { TargetAndTransition, Transition } from \"motion/react\"\nimport { motion, useReducedMotion } from \"motion/react\"\nimport { ChevronLeft, ChevronRight } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/* Motion calendar family: 5 month grids that animate the month change in different ways. Determinism: the visible month derives from a FIXED constant (January 2026), NOT from the clock. The month state is held OUTSIDE the grid; only the grid is remounted on the month key, so prev and next always work. Motion is disabled with useReducedMotion(). Colour comes ONLY from semantic tokens. */\n\nexport type StyledSize = \"sm\" | \"md\" | \"lg\" | \"xl\"\n\nconst MONTHS = [\n  \"January\",\n  \"February\",\n  \"March\",\n  \"April\",\n  \"May\",\n  \"June\",\n  \"July\",\n  \"August\",\n  \"September\",\n  \"October\",\n  \"November\",\n  \"December\",\n] as const\n\nconst WEEKDAYS = [\"Mo\", \"Tu\", \"We\", \"Th\", \"Fr\", \"Sa\", \"Su\"] as const\n\nconst FIXED_MONTH = new Date(2026, 0, 1)\n\nfunction startOfMonth(d: Date) {\n  return new Date(d.getFullYear(), d.getMonth(), 1)\n}\n\nfunction addMonths(d: Date, n: number) {\n  return new Date(d.getFullYear(), d.getMonth() + n, 1)\n}\n\nfunction isSameDay(a: Date | null | undefined, b: Date | null | undefined) {\n  return (\n    !!a &&\n    !!b &&\n    a.getFullYear() === b.getFullYear() &&\n    a.getMonth() === b.getMonth() &&\n    a.getDate() === b.getDate()\n  )\n}\n\nfunction buildMonthGrid(month: Date): Date[] {\n  const first = startOfMonth(month)\n  const offset = (first.getDay() + 6) % 7\n  const start = new Date(first)\n  start.setDate(first.getDate() - offset)\n  const days: Date[] = []\n  for (let i = 0; i < 42; i++) {\n    const d = new Date(start)\n    d.setDate(start.getDate() + i)\n    days.push(d)\n  }\n  return days\n}\n\nfunction toWeeks(days: Date[]): Date[][] {\n  const weeks: Date[][] = []\n  for (let i = 0; i < days.length; i += 7) weeks.push(days.slice(i, i + 7))\n  return weeks\n}\n\nfunction ariaLabel(d: Date) {\n  return `${MONTHS[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`\n}\n\n/* Ay + yon durumu. Animasyonlu alt agacin DISINDA yasar. */\nfunction useVisibleMonth(defaultMonth?: Date) {\n  const [visible, setVisible] = React.useState<Date>(() =>\n    startOfMonth(defaultMonth ?? FIXED_MONTH)\n  )\n  const [dir, setDir] = React.useState(0)\n  const goPrev = React.useCallback(() => {\n    setDir(-1)\n    setVisible((m) => addMonths(m, -1))\n  }, [])\n  const goNext = React.useCallback(() => {\n    setDir(1)\n    setVisible((m) => addMonths(m, 1))\n  }, [])\n  return { visible, dir, goPrev, goNext }\n}\n\n/* Secili gun durumu da izgaranin disinda yasar; ay degisiminde kaybolmaz. */\nfunction useSelected(defaultValue?: Date, onSelect?: (d: Date) => void) {\n  const [selected, setSelected] = React.useState<Date | undefined>(defaultValue)\n  const select = React.useCallback(\n    (d: Date) => {\n      setSelected(d)\n      onSelect?.(d)\n    },\n    [onSelect]\n  )\n  return { selected, select }\n}\n\nconst cellSizes: Record<StyledSize, string> = {\n  sm: \"size-8 text-xs\",\n  md: \"size-9 text-sm\",\n  lg: \"size-10 text-sm\",\n  xl: \"size-11 text-base\",\n}\n\nconst navButton =\n  \"inline-flex size-7 items-center justify-center rounded-md text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 [&_svg]:size-4 [&_svg]:shrink-0 [&_i]:text-base [&_i]:leading-none\"\n\nconst dayBase =\n  \"relative inline-flex items-center justify-center rounded-md font-normal text-foreground outline-none transition-colors duration-(--motion-fast) ease-(--motion-ease) hover:bg-accent hover:text-accent-foreground focus-visible:z-10 focus-visible:ring-[3px] focus-visible:ring-ring/50 motion-reduce:transition-none\"\n\nconst daySelected =\n  \"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground\"\n\nconst dayOutside = \"text-muted-foreground/50\"\n\ninterface MonthMotion {\n  initial: TargetAndTransition\n  animate: TargetAndTransition\n  transition: Transition\n}\n\nexport interface MotionCalendarProps {\n  className?: string\n  size?: StyledSize\n  defaultMonth?: Date\n  defaultValue?: Date\n  onSelect?: (d: Date) => void\n}\n\n/* Shared body. buildMotion produces the month grid's entry animation from the direction; no animation at all is applied under reduced motion. */\nfunction MotionCalendarBase({\n  props,\n  buildMotion,\n  perspective,\n}: {\n  props: MotionCalendarProps\n  buildMotion: (dir: number) => MonthMotion\n  perspective?: boolean\n}) {\n  const { className, size = \"md\", defaultMonth, defaultValue, onSelect } = props\n  const { visible, dir, goPrev, goNext } = useVisibleMonth(defaultMonth)\n  const { selected, select } = useSelected(defaultValue, onSelect)\n  const reduce = useReducedMotion()\n  const titleId = React.useId()\n  const weeks = toWeeks(buildMonthGrid(visible))\n  const monthKey = `${visible.getFullYear()}-${visible.getMonth()}`\n  const m = buildMotion(dir)\n\n  return (\n    <div\n      data-slot=\"styled-calendar\"\n      role=\"grid\"\n      aria-labelledby={titleId}\n      className={cn(\n        \"inline-block rounded-xl border border-border bg-card p-3 text-card-foreground\",\n        className\n      )}\n    >\n      <div className=\"mb-2 flex items-center justify-between gap-2 px-1\">\n        <button type=\"button\" onClick={goPrev} aria-label=\"Previous month\" className={navButton}>\n          <ChevronLeft />\n        </button>\n        <div id={titleId} aria-live=\"polite\" className=\"text-sm font-medium\">\n          {MONTHS[visible.getMonth()]} {visible.getFullYear()}\n        </div>\n        <button type=\"button\" onClick={goNext} aria-label=\"Next month\" className={navButton}>\n          <ChevronRight />\n        </button>\n      </div>\n      <div role=\"row\" className=\"grid grid-cols-7 gap-1\">\n        {WEEKDAYS.map((w) => (\n          <div\n            key={w}\n            role=\"columnheader\"\n            className=\"flex items-center justify-center py-1 text-xs font-medium text-muted-foreground\"\n          >\n            {w}\n          </div>\n        ))}\n      </div>\n      <div\n        className=\"relative overflow-hidden\"\n        style={perspective && !reduce ? { perspective: \"800px\" } : undefined}\n      >\n        <motion.div\n          key={monthKey}\n          initial={reduce ? false : m.initial}\n          animate={reduce ? {} : m.animate}\n          transition={reduce ? { duration: 0 } : m.transition}\n        >\n          {weeks.map((week) => (\n            <div key={week[0].toISOString()} role=\"row\" className=\"grid grid-cols-7 gap-1\">\n              {week.map((d) => {\n                const outside = d.getMonth() !== visible.getMonth()\n                const isSelected = isSameDay(d, selected)\n                return (\n                  <button\n                    key={d.toISOString()}\n                    type=\"button\"\n                    role=\"gridcell\"\n                    aria-label={ariaLabel(d)}\n                    aria-selected={isSelected}\n                    onClick={() => select(d)}\n                    className={cn(\n                      dayBase,\n                      cellSizes[size],\n                      outside && dayOutside,\n                      isSelected && daySelected\n                    )}\n                  >\n                    {d.getDate()}\n                  </button>\n                )\n              })}\n            </div>\n          ))}\n        </motion.div>\n      </div>\n    </div>\n  )\n}\n\n/* ---- 1. SlideCalendar: yon bilgisine gore yatay kayma ---- */\n\nexport function SlideCalendar(props: MotionCalendarProps) {\n  return (\n    <MotionCalendarBase\n      props={props}\n      buildMotion={(dir) => ({\n        initial: { x: dir >= 0 ? \"20%\" : \"-20%\", opacity: 0 },\n        animate: { x: 0, opacity: 1 },\n        transition: { duration: 0.24, ease: \"easeOut\" },\n      })}\n    />\n  )\n}\n\n/* ---- 2. FadeCalendar: an opacity transition only ---- */\n\nexport function FadeCalendar(props: MotionCalendarProps) {\n  return (\n    <MotionCalendarBase\n      props={props}\n      buildMotion={() => ({\n        initial: { opacity: 0 },\n        animate: { opacity: 1 },\n        transition: { duration: 0.28, ease: \"easeOut\" },\n      })}\n    />\n  )\n}\n\n/* ---- 3. PopCalendar: kucukten normale olcek + opaklik ---- */\n\nexport function PopCalendar(props: MotionCalendarProps) {\n  return (\n    <MotionCalendarBase\n      props={props}\n      buildMotion={() => ({\n        initial: { scale: 0.92, opacity: 0 },\n        animate: { scale: 1, opacity: 1 },\n        transition: { duration: 0.22, ease: \"easeOut\" },\n      })}\n    />\n  )\n}\n\n/* ---- 4. SpringCalendar: yay fizigiyle dikey giris ---- */\n\nexport function SpringCalendar(props: MotionCalendarProps) {\n  return (\n    <MotionCalendarBase\n      props={props}\n      buildMotion={(dir) => ({\n        initial: { y: dir >= 0 ? 18 : -18, opacity: 0 },\n        animate: { y: 0, opacity: 1 },\n        transition: { type: \"spring\" as const, stiffness: 320, damping: 24, mass: 0.7 },\n      })}\n    />\n  )\n}\n\n/* ---- 5. MorphCalendar: 3B egilme + olcek ile donusum ---- */\n\nexport function MorphCalendar(props: MotionCalendarProps) {\n  return (\n    <MotionCalendarBase\n      props={props}\n      perspective\n      buildMotion={(dir) => ({\n        initial: { rotateX: dir >= 0 ? -16 : 16, scale: 0.96, opacity: 0 },\n        animate: { rotateX: 0, scale: 1, opacity: 1 },\n        transition: { duration: 0.32, ease: \"easeOut\" },\n      })}\n    />\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ui/calendar-motion.tsx"
    }
  ],
  "categories": [
    "styled",
    "calendar"
  ],
  "type": "registry:component"
}